你需要知道关于C语言指针的一切

Everything you need to know about pointers in C

你需要知道关于C语言指针的一切

指针的定义

指针是内存地址。
(嗯,简短的段落。)


开始

假设你声明一个名为foo的变量。

int foo;

这个变量占用一些内存。 在当前主流的Intel处理器上,它占用四个字节的内存(因为int是四个字节宽)。
现在让我们声明另一个变量。

int *foo_ptr = &foo;

foo_ptr被声明为指向int的指针。我们已经初始化它指向foo。
正如我所说,foo占据一些记忆。它在内存中的位置称为其地址。 &foo是foo的地址(这就是为什么&被称为“地址操作符”)。
把每个变量想象成一个盒子。 foo是sizeof(int)字节大小的盒子。此框的位置是其地址。当您访问地址时,实际访问它指向的框的内容。
这是所有变量的真实,不管类型。事实上,从语法上讲,没有像“指针变量”这样的东西:所有的变量都是一样的。但是,有不同类型的变量。 foo的类型是int.foo_ptr的类型是int *。 (因此,“指针变量”真的意味着“指针类型的变量”。)
这就是说,指针不是变量!指向foo的指针是foo_ptr的内容。你可以在foo_ptr框中放一个不同的指针,框仍然是foo_ptr。但它不会再指向foo.



指针也有一个类型,顺便说一句。 它的类型是int。 因此,它是一个“int指针”(int指针)。 int **的类型是int *(它指向int的指针)。 指针对指针的使用称为多重间接。 更多关于这一点。


插曲:声明语法

在单个声明中声明两个指针变量的明显方法是:

int* ptr_a, ptr_b;

  • 如果包含指向int的指针的变量的类型是int *,
并且单个声明可以通过简单地提供逗号分隔列表(ptr_a,ptr_b)来声明相同类型的多个变量,
那么可以通过简单地给出int指针类型(int *),然后使用逗号分隔的变量列表(ptr_a,ptr_b)来声明多个int指针变量。
鉴于此,ptr_b的类型是什么? int *,对不对?
* bzzt *错误!
ptr_b的类型为int。 它不是指针。
C的声明语法忽略指针星号时携带类型到多个声明。 如果你将ptr_a和ptr_b的声明分成多个声明,你会得到这样:

int *ptr_a;int ptr_b;

可以把它看作一个基本类型(int),加上一个间接级别,用星号(ptr_b的值为0,ptr_a的值为1)表示。
有可能以清晰的方式做单行声明。 这是立即改进:

int *ptr_a, ptr_b;

请注意,星号已移动。 它现在紧挨着词ptr_a。 关联的微妙含义。
将非指针变量放在第一位甚至更清楚:

int ptr_b, *ptr_a;

绝对最清楚的是保持每个声明在其自己的线上,但是可以占用很多垂直空间。 只使用你自己的判断。
最后,我应该指出,你可以做到这一点很好:

int *ptr_a, *ptr_b;

没有什么问题。
顺便说一下,C允许变量名和星号周围的没有或多个级别的括号:

int ((not_a_pointer)), (*ptr_a), (((*ptr_b)));

这对任何东西都没有用,除了声明函数指针(稍后描述)。
进一步阅读:读取C声明的右左规则。


赋值和指针

现在,如何为这个指针指定一个int? 这个解决方案可能很明显:

foo_ptr = 42;

这也是错误的。
对指针变量的任何直接赋值都将改变变量中的地址,而不是该地址处的值。 在这个例子中,foo_ptr的新值(即该变量中的新“指针”)为42.但我们不知道这指向任何东西,所以它可能不是。 尝试访问此地址可能会导致细分违规(阅读:崩溃)。
(顺便说一下,编译器通常会在你试图为一个指针变量赋值时发出警告,gcc将会说“warning:initialization让指针从整型变为无转义”。)
那么如何在指针访问值呢? 您必须取消引用它。


解除引用

int bar = *foo_ptr;

在此声明中,取消引用运算符(前缀*,不要与乘法运算符混淆)查找存在于地址处的值。 (这被称为“加载”操作。)
它也可以写一个解引用表达式(C的方式说:解引用表达式是一个左值,意味着它可以出现在一个赋值的左边):

*foo_ptr = 42; Sets foo to 42

(这被称为“存储”操作。)


插段:数组

这里是一个三int数组的声明:

int array[] = { 45, 67, 89 };

注意,我们使用[]符号,因为我们声明一个数组。 int * array在这里是非法的;编译器不会接受我们为其分配{45,67,89}初始化器。
这个变量,数组,是一个超大的盒子:三个int值的存储。
C的一个整洁的特性是,在大多数地方,当你再次使用名称数组,你实际上将使用指向它的第一个元素的指针(在C术语,&array [0])。这被称为“衰变”:数组“衰减”为指针。数组的大多数用法等于if数组已被声明为指针。
当然,有不等同的情况。一个是自己分配给名称数组(array = ...) - 这是非法的。
另一个是将其传递给sizeof运算符。结果将是数组的总大小,而不是指针的大小(例如,使用上面的数组的sizeof(array)将在当前Mac OS X上评估为(sizeof(int)= 4)×3 = 12系统)。这说明你真的处理一个数组,而不仅仅是一个指针。
然而,在大多数使用中,数组表达式的工作方式与指针表达式相同。
所以,例如,让我们说,你想传递一个数组到printf。你不能:当你传递一个数组作为参数到一个函数,你真的传递一个指针到数组的第一个元素,因为数组衰减到一个指针。你只能给printf指针,而不是整个数组。 (这就是为什么printf没有办法打印数组:它需要你告诉它的数组中的内容的类型和有多少个元素,并且格式字符串和参数列表将很快令人困惑。)
腐烂是一种隐性的array ==&array ==&array [0]。在英语中,这些表达式读取“array”,“指向数组的指针”和“指向数组的第一个元素的指针”(下标运算符[])的优先级高于操作符的地址。但在C中,所有三个表达式都意味着相同的东西。
(如果“array”实际上是一个指针变量,那么它们并不意味着相同的事情,因为指针变量的地址不同于其中的地址 - 因此,中间表达式&array不会等于另外两个表达式。只有当数组真的是一个数组时,三个表达式都是相等的。)


指针算术(或:为什么1 == 4)

假设我们要打印出数组的所有三个元素。

int *array_ptr = array;printf(" first element: %i\n", *(array_ptr++));printf("second element: %i\n", *(array_ptr++));printf(" third element: %i\n", *array_ptr);

first element: 45second element: 67 third element: 89

如果你不熟悉++操作符:它加1到一个变量,同变量+ = 1(记住,因为我们使用后缀表达式array_ptr ++,而不是前缀表达式++ array_ptr,表达式计算到array_ptr之前的值增加,而不是之后)。
但是我们在这里做了什么?
嗯,指针的类型很重要。这里的指针类型是int。当您添加到指针或从指针减去时,您执行的量乘以指针类型的大小。在我们的三个增量的情况下,您添加的每个1乘以sizeof(int)。
顺便说一句,虽然sizeof(void)是非法的,void指针递增或递减1个字节。
如果你想知道1 == 4:记住,早些时候,我提到int是目前的英特尔处理器的四个字节。因此,在具有这样的处理器的机器上,从int指针加1或减1,将其改变4个字节。因此,1 == 4.(程序员幽默。)


索引

printf("%i\n", array[0]);

好吧...刚刚发生了什么?
这发生过:

45

好吧,你可能想到了。 但是这与指针有什么关系?
这是C的另一个秘密。下标运算符(数组[0]中的[])与数组无关。
哦,当然,这是它最常见的用法。 但请记住,在大多数上下文中,数组衰减到指针。 这是其中之一:这是一个传递给该运算符的指针,而不是数组。
作为证据,我提交:

int array[] = { 45, 67, 89 };int *array_ptr = &array[1];printf("%i\n", array_ptr[1]);

89

那可能会大脑弯曲一点。 这是一个图:

数组指向数组的第一个元素; array_ptr设置为&array [1],因此它指向数组的第二个元素。 因此,array_ptr [1]等价于array [2](array_ptr从数组的第二个元素开始,因此array_ptr的第二个元素是数组的第三个元素)。
另外,你可能会注意到,因为第一个元素是sizeof(int)字节宽(是一个int),第二个元素是sizeof(int)字节前面的数组的开始。 你是正确的:array [1]相当于*(array + 1)。 (记住,添加到指针或从指针减去的数字乘以指针类型的大小,因此“1”将sizeof(int)字节添加到指针值。)


插曲:结构和联合

C中最有趣的两种类型是结构和联合。 您使用struct关键字创建一个结构类型,并使用union关键字创建联合类型。
这些类型的确切定义超出了本文的范围。 只需说一个结构或联合的声明就像这样:

struct foo {size_t size;char name[64];int answer_to_ultimate_question;unsigned shoe_size;};

块中的每个声明都称为成员。 联合也有成员,但是使用方式不同。 访问成员如下所示:

struct foo my_foo;my_foo.size = sizeof(struct foo);

表达式my_foo.size访问my_foo的成员大小。
那么,如果你有一个结构的指针,你该怎么办?

One way to do it(*foo_ptr).size = new_size;

但是有一个更好的方法,专门为此目的:指针到成员运算符。

Yummyfoo_ptr->size = new_size;

不幸的是,它并不看起来好多多间接。

Icky(*foo_ptr_ptr)->size = new_size; One way(**foo_ptr_ptr).size = new_size; or another

抱怨:Pascal做得更好。 它的dereference运算符是后缀^:

Yummyfoo_ptr_ptr^^.size := new_size;

(但抛开这个抱怨,C是一个更好的语言。)


多级间接地址


我想更多地解释多个间接地址
考虑下面的代码:

int a = 3;int *b = &a;int **c = &b;int ***d = &c;

下面是这些指针的值如何相等:

  • *d == c; 解除引用an (int ***) once gets you an (int **) (3 - 1 = 2)
  • **d == *c == b; 解除引用an (int ***) twice, or an (int **) once, gets you an (int *) (3 - 2 = 1; 2 - 1 = 1)
  • ***d == **c == *b == a == 3; 解除引用 an (int ***) thrice, or an (int **) twice, or an (int *) once, gets you an int (3 - 3 = 0; 2 - 2 = 0; 1 - 1 = 0)

因此,&运算符可以被认为是添加星号(增加指针级别,因为我称之为),和*, - >和[]运算符作为删除星号(减少指针水平)。


指针和const

当涉及指针时,const关键字有点不同。 这两个声明是等效的:

const int *ptr_a;int const *ptr_a;

然而,这两个不是等价的:

int const *ptr_a;int *const ptr_b;

在第一个例子中,int(即* ptr_a)是const; 你不能做* ptr_a = 42。在第二个例子中,指针本身是const; 你可以改变* ptr_b很好,但你不能改变(使用指针算术,例如ptr_b ++)指针本身。


函数指针

注意:所有这些的语法似乎有点异国情调。 它是。 它混淆了很多人,甚至C的骑士。 熊与我。

也可以取一个函数的地址。 并且,与数组类似,当使用它们的名称时,函数衰减到指针。 所以如果你想要的地址,说,strcpy,你可以说strcpy或&strcpy。 (&strcpy [0]不会工作,很明显的原因。)
当调用函数时,使用一个称为函数调用操作符的操作符。 函数调用操作符在其左侧有一个函数指针。
在这个例子中,我们将dst和src作为内部参数传递,并将strcpy作为函数(即函数指针)调用:

enum { str_length = 18U }; Remember the NUL terminator!char src[str_length] = "This is a string.", dst[str_length];strcpy(dst, src); The function call operator in action (notice the function pointer on the left side).

有一个特殊的语法用于声明类型为函数指针的变量。

char *strcpy(char *dst, const char *src); 一个普通的函数声明,供参考char *(*strcpy_ptr)(char *dst, const char *src); Pointer to strcpy-like functionstrcpy_ptr = strcpy;strcpy_ptr = &strcpy; This works toostrcpy_ptr = &strcpy[0]; But not this

请注意上面声明中* strcpy_ptr周围的括号。 这些从星号指示返回类型(char *)的星号指示变量的指针级别(* strcpy_ptr - 一个级别,指向函数的指针)。
此外,就像在常规函数声明中一样,参数名称是可选的:

char *(*strcpy_ptr_noparams)(char *, const char *) = strcpy_ptr; Parameter names removed — still the same type

指向strcpy的指针的类型是char *(*)(char *,const char *); 你可能会注意到这是上面的声明,减去变量名。 你可以在转换中使用它。 例如:

strcpy_ptr = (char *(*)(char *dst, const char *src))my_strcpy;

正如你所期望的,指向函数的指针的指针在括号内有两个星号:

char *(**strcpy_ptr_ptr)(char *, const char *) = &strcpy_ptr;

我们可以有一个函数指针数组:

char *(*strcpies[3])(char *, const char *) = { strcpy, strcpy, strcpy };char *(*strcpies[])(char *, const char *) = { strcpy, strcpy, strcpy }; Array size is optional, same as everstrcpies[0](dst, src);

这是一个病理声明,取自C99标准。 “[这个声明]声明一个没有参数返回int的函数f,没有返回指向int的参数的参数指定的函数fip和一个没有返回int的参数指定的函数的pointerpfi”“(6.7.5.3 [ 16])

int f(void), *fip(), (*pfi)();

换句话说,上面的等价于以下三个声明:

int f(void);int *fip(); Function returning int pointerint (*pfi)(); Pointer to function returning int

但如果你认为这是心灵弯曲,支撑自己...


函数指针甚至可以是函数的返回值。 这部分是真正的心灵弯曲,所以伸展你的大脑有点,以免造成伤害。
为了解释这一点,我将总结你迄今为止学到的所有声明语法。 首先,声明一个指针变量:

char *ptr;

这个声明告诉我们指针类型(char),指针级(*)和变量名(ptr)。 后两个可以进括号:

char (*ptr);

如果我们用名称后面跟一组参数替换第一个声明中的变量名,会发生什么?

char *strcpy(char *dst, const char *src);

嗯。 函数声明。
但是我们也删除了*指示指针级别 - 记住这个函数声明中的*是函数返回类型的一部分。 所以如果我们添加指针级星号回来(使用括号):

char *(*strcpy_ptr)(char *dst, const char *src);

一个函数指针变量!
但等一下。 如果这是一个变量,并且第一个声明也是一个变量,我们不能用一个名称和一组参数替换THIS声明中的变量名称?
我们可以! 结果是返回一个函数指针的函数的声明:

char *(*get_strcpy_ptr(void))(char *dst, const char *src);

请记住,指向不带参数并返回int的函数的指针的类型是int(*)(void)。 所以这个函数返回的类型是char *(*)(char *,const char *)(同样,inner *表示指针,outer *表示指向函数的返回类型的一部分) 。 你可能还记得这也是strcpy_ptr的类型。
所以这个没有参数调用的函数返回一个指向strcpy-like函数的指针:

strcpy_ptr = get_strcpy_ptr();

因为函数指针语法是如此令人费解,大多数开发人员使用typedef来抽象它们:

typedef char *(*strcpy_funcptr)(char *, const char *);strcpy_funcptr strcpy_ptr = strcpy;strcpy_funcptr get_strcpy_ptr(void);


字符串(和为什么没有这样的东西)

C中没有字符串类型。
现在你有两个问题:
1.如果没有字符串类型,为什么我总是看到对“C字符串”的引用?
2.这与指针有什么关系?
事实是,“C字符串”的概念是虚构的(除了字符串字面量)。 没有字符串类型。 C字符串实际上只是字符数组:

char str[] = "I am the Walrus";

此数组的长度为16个字节:“I am the Walrus”为15个字符,加上NUL(字节值为0)终止符。 换句话说,str [15](最后一个元素)是0.这是如何“信号”的结尾。
此成语是C具有字符串类型的程度。 但这就是:成语。 除了它支持:
.前面提到的字符串文字语法
.字符串库
string.h中的函数用于字符串操作。 但是怎么可能,如果没有字符串类型?
为什么,他们工作指针。
这里有一个简单函数strlen的一个可能的实现,它返回一个字符串(不包括NUL终止符)的长度:

size_t strlen(const char *str) { Note the pointer syntax heresize_t len = 0U;while(*(str++)) ++len;return len;}

注意使用指针运算和取消引用。 这是因为,尽管函数的名字,这里没有“字符串”; 只有一个指向至少一个字符的指针,最后一个为0。
这里有另一个可能的实现:

size_t strlen(const char *str) {size_t i;for(i = 0U; str[i]; ++i);When the loop exits, i is the length of the stringreturn i;}

那一个使用索引。 其中,正如我们早先发现的,使用指针(不是数组,绝对不是字符串)。



展开阅读全文

没有更多推荐了,返回首页