C语言基础 - 变量和指针有问题

3
我有些学习C语言的问题,但我没有其他可以求助的地方。我之前使用过JavaScript和Python等面向对象编程语言,所以C语言对我来说是一个很大的改变,我在学习基础知识时遇到了一些困难。我最初使用Zed Shaw的《学习C语言的艰难之路》,但他并没有真正教授任何东西。他让你写很多代码并进行修改,但我并不知道这些代码为什么有效,这只会导致更多的混淆,因为示例变得越来越复杂。
我主要遇到的问题是变量和指针之间的区别(我认为它们之间的区别很明显,但我看到一些示例后,这两者之间的界限完全被模糊了)。
例如,我知道声明和初始化一个名为a的int类型变量和一个指针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);

这个函数如何更新整数变量i的值,当使用&引用变量的内存地址时,而不是变量的值呢?如果涉及到数组和结构体,情况会变得更加复杂。因此,我不得不停下来寻求一些建议。
我真的感到很尴尬,因为我发布了一个初学者的问题,但是每个人都是从某个地方开始的。在进一步学习之前,我知道我需要理解这些基础知识,但是当我看到代码示例与我刚学到的知识相矛盾时,这让我很难理解。我知道这是一个非常普遍的问题,但我希望你们中的一些人可以解释这些基础知识,或者指引我去更好地学习和理解这些知识。我遇到的大多数视频教程都太泛泛而谈了,网上的文本教程也一样,他们只是告诉你如何做某事,而不是解释它,这会在以后引起一些严重的问题。

为什么不看一下那些函数 scanfprintf 接受什么以及它们在处理数字和字符串时所做的事情呢? - Uchia Itachi
4
让我告诉你:每个人都曾像这样与指针挣扎过,这很正常,不必感到尴尬。在我开始学习C语言之前,我已经写了多年的Java,但花了我几周时间才真正明白那些&和*符号到底在干什么。 - Glandy
谢谢@Glandy,很高兴听到我不是唯一一个遇到这个问题的人。我想我只能进行大量实验来解决这个问题。 - samrap
指针乐趣 <tm> 在这里:http://cslibrary.stanford.edu/104/ - alk
我在高中时直接从QBASIC(没有OO)转到C语言,花了一个星期才理解指针。不要担心,因为你问这个问题可能会帮助其他人节省时间。 - nonsensickle
为什么要使用指针? - Grijesh Chauhan
6个回答

6
我将尝试用与其他人略有不同的方式解释这个问题。
考虑到您是从JavaScript和Python过来的,当谈论指针时,我会避免使用"reference"这个术语,因为尽管它们很相似,但并不完全相同。
指针是一个存储地址的变量。就这么简单。指向int的指针存储了int存储的地址。
当您对指针进行解引用时,您告诉编译器操作的不是地址,而是存储在那个地址上的内容。
参考链接:什么是指针?解引用指针
int *p;
int a = 7;

p = &a;
*p = 5;
*p = 5告诉编译器去存储在p内部存储的地址,并将值5存储在那里(作为整数,因为指针p指向整数)。因此,无论在哪里使用变量,我们都可以使用解引用的指向变量的指针来代替它们。
应该使用:
int *p;

p = 5;

然后你将会把一个地址(内存位置)5(无论它在内存中的具体位置是哪里)分配给指针。如果你尝试使用不允许的内存位置,你的程序很可能会崩溃。 Address of & 运算符用来获取某个变量的地址。它并不关心这是什么类型的变量,甚至可以用来获取指针变量的地址。
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知道你变量的地址,它可以用正确的值来更新它们。


@GrijeshChauhan 感谢您提供的链接,它们真的很好。希望我能够+1您的编辑。 - nonsensickle
感谢大家提供的所有精彩答案。我从中学到了很多东西,但这个回答最为详细并且提供了最多的例子。非常感谢你们的帮助 :) - samrap

3
我会回答你关于为什么printf不传递解引用的指针,而scanf传递地址,并希望这样可以使一些事情更加清晰。


首先,按照惯例,C风格的字符串是存储在内存中的字符数组,以 \0 字符(称为NUL终止符)结尾。跟踪C风格字符串的方法是通过保留对字符串第一个字符的指针。

当调用 printf("%s\n", some_str) 时,其中 some_str 的类型为 char*printf 所做的就是打印由 some_str 指向的字符,然后是它后面的字符(在内存中,通过简单地递增指针 some_str 定位),接着再是其后面的字符,直到找到一个 \0,然后停止打印。

其他 C 风格的字符串操作函数(如 strcpystrlen等)使用相同的过程。

之所以这样做的原因是字符串往往具有不同的长度,因此需要不同数量的内存,但是具有变量大小的数据类型非常不方便。因此,我们有了一种内存中字符串格式的约定(实际上是 \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语言程序设计》。这比在线教程更好,并且长期来看很值得。

1
真糟糕,你比我先回答了。你的答案实际上教给他一些东西,所以点赞。 - nonsensickle
感谢您对字符串的背景知识。 - samrap

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并分配给i。请注意,推送到堆栈上的是值11的副本。
如果您希望函数更改参数的值,则不能在堆栈上推送变量的副本。相反,您需要传递变量的内存地址的副本。
int foo(int *p) { *p = 12; return 11; }

int j = 10;
int i = foo(&j);

现在j将为12而i将为11

scanf比我的例子要稍微复杂一些,它可以接受可变数量的参数,并使用第一个参数("%d")作为格式说明符来确定后续参数的数据类型和大小。%d告诉它要期望一个int指针。


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
}

1
如何阅读一些关于指针的基础知识?

http://cslibrary.stanford.edu/106/

我会在下面回答你的问题:

What confuses me is when you declare variables that look like pointers, but aren’t really pointers at all (or are they?). For example:

char *string = “This is a string”;
printf(“%s\n”, string);

是的,它们是!这是一个字符指针。

当取地址符应该引用变量的内存位置而不是值时,此函数如何更新整数i的值?

这取决于函数签名。如果您查看scanf接受的参数列表,就会对您清晰明了。


“它是一个字符指针,并被分配给”这句话的意思是将字符指针分配给某个字面量。 - alk
不是某个'变量'的地址,而是字面字符串”这句话解释得不太清楚...我不理解它的意思。 - nonsensickle
@nonsensickle,附上更多解释的链接https://dev59.com/23E95IYBdhLWcg3wlu6z - Digital_Reality

1
    char *string = "This is a string";
    printf("%s\n", string);
What is string when it is defined and initialized? Is it a pointer? And if it is, why don’t you dereference it when printing it in the printf function? There are many examples like this that confuse me.

首先,简要介绍一下背景:

除非它是sizeof或一元&运算符的操作数,或者是一个字符串字面值用于声明中初始化另一个数组,类型为 "N个元素的T数组" 的表达式将被转换("衰减")为类型为 "指向T的指针" 的表达式,并且表达式的值是数组的第一个元素的地址。

字符串字面值"This is a string"是类型为 "17个元素的char数组" (16个字符加上0终止符)的表达式。由于它既不是sizeof或一元&运算符的操作数,也没有用于初始化char数组,因此表达式的类型被转换为 "指向char的指针",并且表达式的值是第一个元素的地址。

变量string声明为类型 "指向char的指针" (char *),并使用字符串字面值的地址进行初始化。

printf函数调用中,转换说明符%s期望其对应的参数具有类型char *;它将打印从指定地址开始的字符,直到遇到0终止符。这就是为什么变量在printf函数调用中不被解引用的原因。
    int i;
    scanf("%d", &i);
How does this function update the value of integer i, when the ampersand is supposed to reference the location in memory of the variable, not the value? It gets even more complicated with arrays and structs, which is where I stopped and decided I needed to find some advice.

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 );

如果您编译并运行此代码,则在两个输出语句中将看到xy的相同值;abxy在内存中是不同的对象,更改ab的值不会影响xy的值。
由于我们想要修改xy的值,因此我们必须向函数传递这些变量的指针,如下所示:
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 );

我们不是交换ab的值,而是交换ab所指向的值;表达式*a*b引用内存中与xy相同的对象。

这就是为什么在scanf调用中必须传递一个指向i的指针(scanf转换说明符%d期望相应的参数具有类型int *);否则,您将无法更新i的值。

我对"Learn C The Hard Way"并不是很印象深刻;然而,几乎所有的C参考书都有一些致命的缺陷。Kernighan和Ritchie的The C Programming Language虽然有点老了(它没有涵盖最近两个标准修订中引入的任何内容),但仍然可能是最好的介绍该语言的总体书籍。我也听说过King的C Programming: A Modern Approach不错。我的首选桌面参考书是Harbison & Steele的C: A Reference Manual,尽管它正是所说的 - 一个参考手册,并因此对解释基本概念不太重视。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接