首页 > 其他分享 >你需要知道关于C语言指针的一切

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

时间:2023-07-14 15:32:09浏览次数:51  
标签:一切 int C语言 char strcpy foo ptr 指针


Everything you need to know about pointers in C


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

指针的定义

指针是内存地址。


(

嗯,简短的段落。)



开始

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

foo;

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


现在让我们声明另一个变量。


foo_ptr


foo_ptr被声明为指向int的指针。我们已经初始化它指向foo。


正如我所说,foo占据一些记忆。它在内存中的位置称为其地址。 &foo是foo的地址(这就是为什么&被称为“地址操作符”)。


把每个变量想象成一个盒子。 foo是sizeof(int)字节大小的盒子。此框的位置是其地址。当您访问地址时,实际访问它指向的框的内容。


这是所有变量的真实,不管类型。事实上,从语法上讲,没有像“指针变量”这样的东西:所有的变量都是一样的。但是,有不同类型的变量。 foo的类型是int.foo_ptr的类型是int *。 (因此,“指针变量”真的意味着“指针类型的变量”。)


这就是说,指针不是变量!指向foo的指针是foo_ptr的内容。你可以在foo_ptr框中放一个不同的指针,框仍然是foo_ptr。但它不会再指向foo.


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


指针也有一个类型,顺便说一句。 它的类型是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让指针从整型变为无转义”。)


那么如何在指针访问值呢? 您必须取消引用它。



解除引用

bar

在此声明中,取消引用运算符(前缀*,不要与乘法运算符混淆)查找存在于地址处的值。 (这被称为“加载”操作。)


它也可以写一个解引用表达式(C的方式说:解引用表达式是一个左值,意味着它可以出现在一个赋值的左边):


Sets foo to 42

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



插段:数组

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

array[]

注意,我们使用[]符号,因为我们声明一个数组。 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)

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

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

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

你需要知道关于C语言指针的一切_指针变量_02


数组指向数组的第一个元素; 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;

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

  • 解除引用an (int ***) once gets you an (int **) (3 - 1 = 2)
  • 解除引用an (int ***) twice, or an (int **) once, gets you an (int *) (3 - 2 = 1; 2 - 1 = 1)
  • 解除引用 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作为函数(即函数指针)调用:


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_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 - 一个级别,指向函数的指针)。


此外,就像在常规函数声明中一样,参数名称是可选的:


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;

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

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

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

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终止符)的长度:


Note the pointer syntax heresize_t len = 0U;while(*(str++)) ++len;return len;}

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


这里有另一个可能的实现:


When the loop exits, i is the length of the stringreturn i;}

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





标签:一切,int,C语言,char,strcpy,foo,ptr,指针
From: https://blog.51cto.com/u_15170706/6722171

相关文章

  • C语言宏定义
    C语言中,宏定义是一种在编译阶段进行文本替换的机制,可以提高代码的可读性、可移植性和方便性。下面是一些在成熟软件中常用的宏定义示例:1.防止头文件被重复包含:```c#ifndefHEADER_FILE_NAME#defineHEADER_FILE_NAME//头文件内容#endif```这种宏定义结构可以防止同一个......
  • 文件位置指针
     istream 和 ostream 都提供了用于重新定位文件位置指针的成员函数。这些成员函数包括关于istream的 seekg("seekget")和关于ostream的 seekp("seekput")。seekg和seekp的参数通常是一个长整型。第二个参数可以用于指定查找方向。查找方向可以是 ios::beg(默认的,从流......
  • C语言-06
    文件操作/*1.文件路径:相对路径/绝对路径2.根目录---如:C盘、D盘、E盘3.操作文件:1.单个字符读写2.字符串读写3.格式化读写4.二进制形式读写5.指定位置读写*/#include<stdio.h>intfunc_1();//声明int......
  • 指针
    指针在我的理解中,指针是用来存放地址的。指针也有大小,对于不同类型的指针char,short,int,,,double,它们的指针大小都是8个字节,对于64位机器是这样的,而对于32位机器它们的指针大小都为4个字节。我的是64位操作系统:#include<stdio.h>intmain(){ printf("%zd\n",sizeof(char*)); ......
  • c语言的内存泄漏问题
    在今天的动态内存分配的学习中,我遇到了内存泄漏问题,自己开辟的空间,自己找不到了,并且系统也无法使用,通过查找资料得到了比较加深的见解。C语言什么是内存泄漏,怎么避免内存泄漏一、内存溢出内存溢出OOM(outofmemory),......
  • C语言-05
    预处理/*---用#号开头的命令是预处理命令---“预处理”即:预先处理,在编译前对代码进行一个预先处理include<文件.h>//这个是文件包含命令总上所述,#include<stdio.h>即是:执行“预处理文件包含stdio.h”*/# include/*当main......
  • Linux C语言之时间函数精讲
    @TOC当在LinuxC语言编程中需要处理时间相关的操作时,可以使用时间函数。时间函数提供了获取当前时间、进行时间转换和计算时间差等功能,对于日志记录、性能分析以及定时任务等场景非常有用。本文将详细介绍LinuxC语言中常用的时间函数,包括如何使用它们以及示例代码。前言在Lin......
  • Java空指针异常优雅处理的方式
    1原因如下:由于Java开发过程中一不注意就会造成空指针异常,但是如果要避免这些空指针异常我们就可能需要写如下啰嗦有无聊的语句:if(test!=null&&test.size()>0){..............}为了避免写这些无聊的语句和避免NPE错误,我们可以用如下用法进行替代。......
  • 助教工作总结(高级语言程序设计C语言)
    一、助教工作的具体职责和任务1、与老师的配合:协助老师指导两次课程设计协助老师指导蓝桥杯、天梯赛2、与课程其他助教的配合:共同解决同学们课后的疑问并将出现的重点问题反馈给老师安排大作业并进行相应的指导,最后进行批改评分将最终成绩反馈给老师一起挑选有深度的题......
  • 现代C++(Modern C++)基本用法实践:五、智能指针(Smart Pointers)
    概述c++效率较高的一个原因是我们可以自己定制策略手动申请和释放内存,当然,也伴随着开发效率降低和内存泄漏的风险。为了减少手动管理内存带来的困扰,c++提出了智能指针,可以帮助我们进行内存管理,有三种:std::unique_ptr是一种独占所有权的智能指针,它不允许多个指针指向同一个对......