我在学习C方面遇到了一些问题而且我真的没有其他地方可以提出建议。我来自一系列OOP语言,例如JavaScript和主要是Python,因此C是一个重大变化,我在尝试学习基础知识方面遇到了一些障碍。我最初是从Zed Shaw的“艰难学习C”开始的,但他并没有在书中教授任何东西。是的,他让你写了很多代码并改变了一些东西,但是我不知道为什么代码会起作用,而这只会导致更复杂的例子。
我遇到的主要问题是变量和指针之间的差异(我认为它是非常独特的,直到我看到一些我将在下面发布的示例,这完全模糊了二)。
例如,我了解声明和初始化名为int
的{{1}}和指针a
将如下所示:
p
让我感到困惑的是,当你声明看起来像指针的变量,但根本不是真正的指针(或者它们是什么?)。例如:
int a;
int *p;
a = 12;
p = &a;
定义和初始化时char *string = "This is a string";
printf("%s\n", string);
是什么?它是指针吗?如果是的话,为什么不在string
函数中打印它时取消引用它?有很多像这样的例子让我很困惑。
我遇到的另一个例子没有任何意义:
printf
当&符号应该引用变量的内存中的位置而不是值时,该函数如何更新整数int i;
scanf("%d", &i);
的值?阵列和结构变得更加复杂,这是我停下来的地方,我决定要找一些建议。
老实说,我觉得很尴尬发布这样一个noob问题,但每个人都从某个地方开始。这些基础知识是我知道在继续之前需要能够理解的东西,但是当我看到与我刚刚学到的内容相矛盾的代码示例时,我很难理解它。我知道这是一个非常普遍的问题,但我希望你们中的一些人能够解释这些基础知识,或者指出我可以更好地学习/理解这一点的方向。我遇到的大多数视频教程都过于笼统,与在线文本教程相同,他们告诉你如何做某事,但不解释它,这会导致一些严重的问题。
答案 0 :(得分:6)
我会尝试和其他人稍微不同地解释一下。
考虑到你来自JavaScript和Python,我会在讨论指针时避免使用术语“引用”。这是因为虽然它们相似但它们并不相同。
指针是存储地址的变量。就这么简单。指向int
的指针存储了存储int
的地址。
当您取消引用指针时,您告诉编译器要操作,而不是在地址上,而是在存储在该地址的内容上。
int *p;
int a = 7;
p = &a;
*p = 5;
*p = 5
告诉编译器转到存储在p
内的地址并将值存储在那里(作为整数,因为指针p
指向一个整数)。因此,无论我们在何处使用变量,我们都可以使用解引用指针代替变量。
你应该使用:
int *p;
p = 5;
然后,您将为指针分配5
(可能在内存中的任何位置)的地址(内存位置)。如果您尝试使用不允许的内存位置,则程序可能会崩溃。
&
运算符获取某个变量的地址。它真的不关心它是什么,它甚至可以接受指针的地址。
int a;
int *p;
int **pp;
pp = &p;
p = &a;
就是这样。
您的示例
字符串示例
char *string = "This is a string";
printf("%s\n", string);
您感到困惑的原因是,在JavaScript和Python中,您可以将字符串视为可以适合变量。它不能!在C中,字符串是按顺序存储在存储器(字符数组)中的字符序列。这就是我们可以使用指针的原因。我们做的是让指针指向第一个字符,然后我们知道整个字符串的位置(因为它是顺序的)。另外,在C中我们不存储字符串的大小。相反,字符串从指针开始,当我们遇到零字节'\0'
或只是0
时结束。
scanf示例
int i;
scanf("%d", &i);
scanf
的地址为i
的原因是因为它会将通过控制台输入的整数放入i
所在的位置。这样scanf
实际上可以有多个返回值,这意味着:
int i, j;
scanf("%d%d", &i, &j);
是可能的。由于scanf
知道变量的地址,因此可以使用正确的值更新它们。
答案 1 :(得分:3)
我将回答您关于为什么printf
没有通过解除引用的指针传递的具体问题,以及为什么scanf
传递了一个地址,希望这会使某些事情变得更清楚。
首先,按照惯例,C样式字符串是内存中的字符数组,以\0
字符(称为NUL终结符)终止。跟踪C风格字符串的方法是保持指向字符串第一个字符的指针。
调用printf("%s\n", some_str)
时,some_str
的类型为char*
,printf
执行的操作是打印some_str
指向的字符,然后是字符在它之后(在内存中,通过简单地递增指针some_str
来定位),然后是之后的字符,直到它找到\0
,然后它停止打印。
其他C风格的字符串操作函数使用相同的过程,例如strcpy
,strlen
等。
这样做的一个原因是字符串往往具有不同的长度,因此需要不同的存储量,但是具有可变大小的数据类型是不方便的。所以,我们有一个内存中字符串格式的约定(本质上是\0
终结符约定),我们只指向带有char*
的字符串。
如果您取消引用char *
,您将得到一个字符,而不是您所期望的字符串,所以请注意这一点。
为什么
scanf("%d", &i)
会将地址传递给int
,而不是int
本身?
原因是在C中,函数参数通过值传递,这意味着当你将某些东西传递给一个函数时,它的一个副本就被生成并传递给函数,无论函数是什么复制品,原件保持不变。这有什么含义?
首先,如果您有以下功能:
void add_two(int i) {
i = i + 2;
}
如果您致电:int i = 3; add_two(i);
,则i
的值不会更改,因为add_two
只获得了i
的副本。另一方面,如果你有一个像:
void really_add_two(int *i) {
*i = *i + 2;
}
现在int i = 3; really_add_two(&i);
会导致i
的值为5
。这是因为i
的指针被赋予really_add_two
,并且该函数修改了该内存位置的数据 (通过取消引用)。
需要为scanf
提供地址的原因与上述相同。
如果你认真学习C,你应该选择Kernighan和Richie,“C编程语言”。它比在线阅读教程要好得多,从长远来看也是值得的。
答案 2 :(得分:2)
指针是一个保存内存地址而不是值的变量。
当你写
int a;
您指定a
可以包含整数
+-----+
| int |
+-----+
a
位于内存中,位于a
的地址
写成&a
+-----+
&a -> | int |
+-----+
当你写
int *p;
您指定p
可以保存指向整数的指针,即内存地址。
+---------+
p -> | int |
+---------+
例如它可以指向a
p = &a;
int i;
scanf( "%d", &i );
为了理解上面你需要了解的功能如何 参数传递给函数,实际函数是什么。
一个函数也是一个指针,所以当你调用一个函数时,你就是这样 告诉编译器你想在某个内存中执行代码 地址。一个堆栈用于传递参数和结果 功能和来电。
所以
int foo(int n) { return 11; }
int j = 10;
int i = foo(j);
告诉编译器应该执行汇编指令
地址foo
,但首先在堆栈上按10(j),推送j的副本
在堆栈上执行foo
,当foo
即将返回时,它会推送
在堆栈上返回11并返回,然后返回值11
打电话给我。注意它是推送的值11的副本
在堆栈上。
如果您希望函数更改参数的值,则不能 在堆栈上推送变量的副本。相反,你需要传递一份副本 变量的内存地址
int foo(int *p) { *p = 12; return 11; }
int j = 10;
int i = foo(&j);
现在j将是12和i 11
scanf
比我的例子复杂一点,它可以使用可变数量的参数并使用第一个参数(“%d”)作为格式说明符来确定以下参数的数据类型和大小。 %d告诉它期望一个int指针。
答案 3 :(得分:1)
定义和初始化时的字符串是什么?它是指针吗?如果是的话,为什么不在printf函数中打印它时取消引用呢?
这是一个指针。它直接指向字符串中的第一个字符。您不必取消引用它,因为printf
将接受char
指针..并打印所有内容,直到找到空终止符(自动为您添加)。
当&符号应该引用变量的内存中的位置而不是值时,该函数如何更新整数i的值?
因为&i
使参数作为int*
传递。因此该函数将取消引用并添加..有点像这样:
// NOTE: not actual scanf implementation
void scanf(char* format, int * input) {
*input = get_number(); // store value in variable
}
答案 4 :(得分:1)
阅读关于指针的一些基础知识怎么样?
我将在下面回答您的问题:
让我感到困惑的是,当你声明看起来像指针的变量,但根本不是真正的指针(或者它们是什么?)。例如:
char *string = “This is a string”; printf(“%s\n”, string);
是的,他们是!它是一个字符指针。
当&符号应该引用变量的内存中的位置而不是值时,该函数如何更新整数
i
的值?
这取决于功能签名。如果你看一下scanf
接受什么作为参数列表,你就会清楚。
答案 5 :(得分:1)
定义和初始化时字符串是什么?它是指针吗?如果是的话,为什么不在printf函数中打印它时取消引用它?有很多像这样的例子让我很困惑。char *string = "This is a string"; printf("%s\n", string);
首先,一点背景:
除非它是sizeof
或一元&
运算符的操作数,或者是用于初始化声明中另一个数组的字符串文字,否则表达式为“N-element array of of T
“将被转换(”衰减“)到”指向T
的指针“类型的表达式,表达式的值将是数组的第一个元素的地址。
字符串文字"This is a string"
是“17个元素数组char
”类型的表达式(16个字符加上0个终止符)。由于它不是sizeof
或一元&
运算符的操作数,并且因为它不用于初始化char
的数组,表达式的类型转换为“指向char
的指针”,表达式的值是第一个元素的地址。
变量string
被声明为“指向char
”(char *
)的类型,并使用字符串文字的地址进行初始化。
在printf
调用中,转换说明符%s
期望其对应的参数具有类型char *
;它将打印从指定地址开始的字符,直到它看到0终止符。这就是printf
调用中未取消引用变量的原因。
当&符号应该引用变量的内存中的位置而不是值时,该函数如何更新整数i的值?阵列和结构变得更加复杂,这是我停下来的地方,我决定要找一些建议。int i; scanf("%d", &i);
C通过值传递所有函数参数,这意味着函数定义中的形式参数和函数调用中的实际参数是内存中的两个不同对象。更新形式参数对实际参数没有影响。例如,想象一个简单的swap
函数,如下所示:
void swap( int a, int b ) { int tmp = a; a = b; b = tmp; return; }
...
int x = 0, y = 1;
printf( "before swap: x = %d, y = %d\n", x, y );
swap( x, y );
printf( " after swap: x = %d, y = %d\n", x, y );
如果编译并运行此代码,则在两个输出语句中都会看到x
和y
的相同值; a
和b
是x
和y
在内存中的不同对象,更改a
和b
的值对此不起作用x
和y
的值。
由于我们要修改x
和y
的值,我们必须将指针传递给函数这些变量,如下所示:
void swap( int *a, int *b ) { int tmp = *a; *a = *b; *b = tmp; return; }
...
int x = 0, y = 1;
printf( "before swap: x = %d, y = %d\n", x, y );
swap( &x, &y );
printf( " after swap: x = %d, y = %d\n", x, y );
我们不是交换a
和b
的值,而是将a
和b
的值交换为; 表达式 *a
和*b
在内存中引用与x
和y
相同的对象。
这就是你必须在i
调用中将指针传递给scanf
的原因(scanf
转换说明符%d
需要相应的参数有类型int *
);否则,您将无法更新i
的值。
我对“学习C艰难之路”印象不深;然后,几乎所有的C引用都有一些致命的缺陷。 Kernighan和Ritchie的The C Programming Language虽然很长(虽然没有涵盖最后两个标准版本中引入的任何内容),但仍然可能是该语言的最佳整体介绍。我也听说过King C Programming: A Modern Approach的好消息。我的桌面参考资料是Harbison& Steele的C: A Reference Manual,虽然它正是它所说的 - 一本参考手册,因此在解释基本概念方面并不那么重要。