推广 热搜: csgo  vue  angelababy  2023  gps  新车  htc  落地  app  p2p 

C 语言中的指针与数组

   2023-07-07 网络整理佚名940
核心提示:则说明pa是一个指向整型数据的指针,那么赋值语句:也就是,一个函数指针变量可以先后指向同类型的不同函数。对于一个数组,如果其元素均为指针类型的数据,称其为“指针数组”,也就是说,指针数组中的每一个元素都相当于一个指针变量,声明方式如下:4)的指针变量,也称为“数组指针”。在理解了指针数组的概念的基础上,下面介绍指向指针数据的指针变量,简称为“指向指针的指针”。例如前面定义的指针数组:

本文主要总结了谭浩强主编的《The C》和《C程序设计》教材中与指针数组相关的章节内容。

在C语言中,指针和数组之间有着非常密切的关系。 一般来说,凡是能通过数组下标完成的操作,都可以通过指针来实现。 本文将介绍指针和数组的概念和关系,以及一些相关问题。 目录如下:

大批

在C语言中,数组用于表示相同类型的有序数据的集合,定义如下:

类型名 数组名[常量表达式];

例如:

int a[10];

意思是定义了一个整型数组,数组名为a,数组中有10个元素。 也就是说,它定义了一个包含10个元素的集合,这些元素存储在相邻的内存区域中,分别命名为a[0]、a[1]、...、a[9],如下图所示:

其中,a[i]表示数组中的第i个元素(i从0开始计数)。

另外,初始化数组元素还有几个规则:

int a[10] = {0123456789};

int a[10] = {01234};

意思是定义a数组有10个元素,并给前5个元素赋初始值,后5个元素的值默认为0。 但我们不能跳转给某些元素赋值,例如int a[5] = {,,3,4,5}; 是一种错误的写作方式。

int a[10] = {0000000000};
int a[10] = {0}; // 与上面等价

请注意,以上是一个特殊情况。 两种写法是等价的,因为int a[10]存储在内存栈上,其所有元素默认为0。第二种写法只是将第一个元素初始化为0。如果把0改为2,则两者不一样相等的。

int a[5] = {12345}; // 可以写成如下形式
int a[] = {12345};  // 声明时省略了数组的长度

上面第二种写法,大括号里有5个数字,编译器在编译时会自动根据这个定义a数组的长度为5。 但是,如果要定义的数组的长度与提供的初始值的数量不同,则不能忽略数组的长度。 比如要定义数组的长度为10,就不能省略数组长度的定义,而必须这样写:int a[10] = {1, 2, 3, 4, 5}; ,表示前五个元素已初始化,后五个元素为0。

指针

指针是保存变量地址的变量,定义如下:

类型名 * 指针变量名;

程序中定义了一个变量。 编译时,系统会为该变量分配内存单元。 编译系统根据程序中定义的变量类型分配一定长度的内存空间(不同类型长度不​​同,一般字符类型为1字节,整型为2或4字节等)。 内存区域中的每个存储单元都有一个编号,这就是“地址”的概念,而指针变量就是用来存储“变量地址”的变量。

如下图所示,如果变量c的类型是char,并且在内存中的存储位置就是图中的位置,我们可以定义一个指针变量p来指向c的存储位置。

char * p = &c;

指针有两个主要运算符:

例如上例中,&c代表变量c的地址,*p代表指针变量p指向的存储单元的内容(即p指向的变量c的值)。

另外,定义指针时声明的类型表示该指针所指向的地址中存储的内容的数据类型。 所有指针变量的类型都是整型,它们占用的大小(字节数)在不同的操作系统中是不同的。

思考一个问题,既然指针变量是用来存储地址的(而且它本身的类型是整数),那么看起来只需要指定为“指针变量”即可,为什么要声明它所指向的内容呢?定义指针类型?

如上所述,不同类型的数据在内存中占用的字节数是不同的,而对于指针的“移动”或者“加减”,比如“将指针移动1个位置”或者“使指针value 加1”,这里的“1”代表什么?

“指针加1”表示与指针所指向的数据相邻的下一个数据的地址。 例如,如果指针指向一个整型变量(假设为2个字节),那么“指针移动1个位置”表示移动2个字节,“指针值增加1”表示使地址值相加2 字节。 而如果指针指向浮点变量(假设为4个字节),则增加的不是2而是4个字节。 因此,必须指定指针变量所指向的变量的类型,即“基类型”,这样才能准确地进行指针的相关位移操作。

最后需要注意的是,指针变量只能指向同一类型的变量,即声明为指向字符类型的指针不能指向整型等其他类型的变量。

指针和数组的关系

第一部分,一个数组int a[10]; 被声明,假设这里我们定义了一个指针变量pa如下:

int *pa;

意思是pa是一个指向整型数据的指针,那么赋值语句:

pa = &a[0];

表示指针pa指向数组a的第0个元素,即pa的值为数组元素a[0]的地址,如下图:

然后是赋值语句 int x = *pa; 表示将数组元素a[0]中的值复制到变量x中,相当于int x = a[0];。

如果pa指向数组中的某个特定元素,那么根据指针算术的定义,pa+1将指向下一个元素,pa+i将指向pa所指向的数组元素之后的第i个元素,并且pa-i 将指向 pa 所指向的数组元素之前的第 i 个元素。

因此,如果指针pa指向a[0],那么*(pa+1)指的是数组元素a[1]的内容,pa+i就是数组元素a[i]的地址,* (pa+i)指的是数组元素a[i]的内容是多少,如下图:

无论数组a中的元素类型或数组长度如何,上述结论都成立。 “指针加1”表示pa+1代表pa指向的元素的下一个元素。

数组下标和指针运算之间存在密切的对应关系。 C语言规定数组名代表数组中第一个元素(即序号为0的元素)的地址。 也就是说,数组名可以理解为指针常量。

因此,执行赋值语句pa = &a[0];后,pa与a的值相同。 因为数组名代表数组第一个元素的地址,因此,pa = &a[0]; 也可以写成 pa = a;。

对数组元素a[i]的引用也可以写成*(a+i)的形式。 事实上,在编译过程中,编译器首先将a[i]转换为*(a+i)的指针表示形式,然后对其求值,因此这两种形式是等价的。

当我们分别对a[i]和*(a+i)进行寻址操作时,可以知道&a[i]和&*(a+i)(简化为a+i)也是一样的,a+ i 表示 a 之后第 i 个元素的地址。

相应地,如果pa是指针,则表达式中也可以在其后面添加下标。 pa[i] 等价于 *(pa+i)。 简而言之,通过(数组和下标)实现的表达式可以等效地通过(指针和偏移量)实现。

最后,需要注意指针和数组名称之间的一个区别:

指针是一个变量,所以赋值语句 pa = a 和自增操作 pa++ 是合法的,但数组名是常量而不是变量,所以像 a = pa 或 a++ 这样的语句是非法的。

以上,希望对大家理解指针和数组的关系有所帮助。

下面我们继续介绍一些与他们两个相关的其他知识点。

字符串和数组

在C语言中,有一种基本数据类型,称为“字符数据”,用char表示。

单引号括起来的字符,如'a'、'A'、'5'、'?'、'\n'、'\0'等都是字符常量,一共有128个符号(其中 32 个符号是不可显示的控制字符)。

字符变量用于存储字符常量,但只能存储一个字符。 不要以为一个字符串(包括几个字符)就可以存储在一个字符变量中。 字符变量定义如下:

char c1, c2;
c1 = 'a';
c2 = 97// 没有看错,这是合法的,c2 变量此时也存放字符 'a'

在所有的编译系统中,都规定用一个字节来存储一个字符,或者说,一个字符变量在内存中占用一个字节。

也许你已经发现,在C语言中,基本数据类型没有“字符串”类型,所以字符串只能存储在“字符数组”中。 字符串常量是用双引号括起来的字符序列。 例如:“你好,世界!”、“CHINA”等等。

注意:不要将字符常量与字符串常量混淆,'a'是字符常量,“a”是字符串常量,两者是不同的,并且不能将字符串常量分配给字符变量。

char c;
c = 'a'// 合法
c = "a"// 错误的
c = "CHINA"// 也是错误的

字符串常量是字符数组。 C语言还规定在每个字符串常量的末尾加一个“字符串结束标志”\0,以便系统判断字符串是否结束。 例如,有一个字符串常量“hello”,它在内存中的存储方式如下:

它占用的内存单元不是5个字符,而是6个字符,最后一个字符是\0。 因此,字符串常量所占用的存储单元数比其字面值(双引号内的字符数)大1。 但是C语言的函数是用来获取字符串长度的,获取到的值不包含终止符\0。

因为C语言中没有“字符串类型”,也就没有真正的“字符串变量”! 本节我们将介绍字符数组的概念以及它们与字符串的关系。

用于存储字符数据的数组称为“字符数组”,字符数组中的每个元素存储一个字符。

正如前面提到的,C 语言将字符串作为字符数组来处理。 因此,对于字符数组来说,如果它的结尾是空字符\0,那么就可以将其视为“字符串变量”(不严格),例如:

char str[6] = {'h''e''l''l''o''\0'};

注意:对于字符数组无论多长,都是从头开始遍历,一旦遇到结束空字符\0,就表示字符串结束,\0之前的字符组成一个字符串,\0之后的字符元素\0 将被忽略。 因此,程序一般通过检测\0的位置来判断字符串是否结束,而不是根据字符数组的长度来判断字符串的长度。

所以有了结束标志\0,字符数组的长度就不再那么重要了。 当然,字符串的实际长度加上\0不能超过存储它的字符数组的长度。

或者,我们可以使用字符串常量来初始化字符数组:

char str[] = {"hello"}; 

这时系统会自动在字符串常量的末尾添加一个终止空字符\0。 也可以省略大括号,直接写:

char str[] = "hello";

以上两种写法相当于下面的初始化写法:

char str[] = {'h''e''l''l''o''\0'};

但并不等同于下面的写法:

char str[] = {'h''e''l''l''o'};

前者的长度为 6,后者的长度为 5。

需要注意的是,对于字符数组来说,并不要求最后一个字符是\0,甚至可以不包含\0。 但如果使用字符数组来存储字符串常量,则末尾必须有一个\0结束标识符,通常系统会自动添加,也可以手动添加。

字符串和指针

上一节提到字符数组可以用来表示和存储字符串,而数组与指针密切相关。 因此,指针也可以用来表示和管理字符串。

对于以下定义:

char str[] = "hello";

我们知道str是数组的名称,它代表字符数组第一个元素的地址。 所以我们可以定义一个字符指针来指向它:

char *pStr = &str[0];

根据前面的讨论,此时pStr = str。 因此,我们可以不定义字符数组,而是直接定义一个字符指针,用字符指针来指向字符串中的字符,如下:

char *pStr = "hello";

其中,pStr指向字符'h'的地址,*pStr即pStr[0]的值为'h',pStr+1指向字符'e'的地址; *(pStr+1)的值,即pStr[1]、'e'等,最后一个字符为结束空字符\0。

这里,pStr只是一个字符指针变量,它指向字符串“hello”的第一个字符的地址(即:存储字符串常量“hello”的字符数组的第一个元素的地址)。 不能理解为:“pStr是一个字符串变量,定义时就将字符“hello”赋值给该字符串变量”。

最后,在C语言中,字符和字符串的输出格式说明符分别为%c和%s,例如:

char c = 'a';
char *str = "hello, world!";
printf("%c", c);
printf("%s", str);

指针常量与常量指针

指针本身是一个常量,它指向的地址不能改变,但所指向地址的内容可以改变,声明方式:

int * const p;

指向常量的指针,也称为常量指针,是指指针所指向的地址对应的值是不可变的,但指针可以指向其他(常量)地址。 声明方法:

int const * p;
const int * p;

const int * const p;

指针函数和函数指针

它是一个函数,即返回指针值的函数。 对于函数来说,它可以返回整数值、字符值、实型值等,也可以返回指针类型的数据,即地址。 申报表如下:

类型名 * 函数名(参数列表)

例子:

int * func(int x, int y);

这里,func是函数名,调用后会得到一个指向整型数据的指针(地址)。

它是一个指针,即指向函数的指针,声明如下:

类型名 (* 指针变量名)(函数参数列表)

例子:

int a, b;
int max(intint);   // 声明一个函数 max(假设已在其它地方实现)
int (* p)(intint); // 声明一个函数指针 *p
p = max;             // 把函数名 max 赋值给函数指针 *p
a = max(12);       // 通过函数名调用
b = (* p)(12);     // 通过函数指针调用

与数组名代表数组第一个元素的地址类似,函数名代表函数的入口地址,赋值语句p = max;的函数就是将函数max的入口地址赋给指针变量p。

此时,p和max都指向函数的开头,调用*p就是调用max函数。

因此,函数调用可以通过函数名来调用,也可以通过函数指针来调用。 上面的a = max(1, 2); 本质上与 b = (* p)(1, 2) 相同。

另外,需要注意的是 int (* p)(int, int); 表示定义了一个指向函数的指针变量p,它不只是指向一个函数,而只是表示定义了这样一种类型的变量,它是专门用来存储函数的入口地址的。 在程序中,将另一个函数的地址(该函数的返回值应该是一个整数,并且有两个整数参数)赋给它,它就会指向这个函数。 即一个函数指针变量可以连续指向同一类型的不同函数。

指针数组与指向指针的指针

对于数组来说,如果其元素是指针类型数据,则称为“指针数组”,也就是说指针数组中的每个元素就相当于一个指针变量,声明方法如下:

类型名 * 数组名[数组长度];

例如:int * p[4]; 意思是声明一个数组p,它有4个元素,每个元素的类型都是int * 指针类型,即每个元素可以指向一个整型变量。

指针数组的使用场景:比较适合存储几个字符串(也可以理解为“字符串数组”),使得字符串处理更加方便灵活。 例如:

char * names[] = {"Li Lei""Han Meimei""Kang Zubin"};

指针数组的另一个重要作用是:作为main函数的形参,如下:

int main (int argc, char * argv[]);

注意,上面不要写成int(*p)[4],它代表的是一个指向一维数组(数组的长度为4)的指针变量,也称为“数组指针”。

在理解指针数组概念的基础上,下面介绍指向指针数据的指针变量,简称“指针到指针”。

例如前面定义的指针数组:

char * names[] = {"Li Lei""Han Meimei""Kang Zubin"};

其示意图如下:

这里,names是一个指针数组,它的每个元素都是一个指针类型的数据(值为地址),指向某个字符串常量(字符数组),数组中的每个元素都有自己的地址。

根据前面的定义,数组名names表示指针数组第一个元素的地址,names+i是元素names[i]的地址。 因此,指针数组的数组名本身就是一个指向指针的指针。

另外,我们还可以定义一个指针变量p来指向指针数组的元素(数组的元素也是指针),那么p就是一个指向指针数据的指针变量。

char ** p = &names[0]; // 等价于
char ** p = names;

上面的p前面有两个*号,相当于*(*p),其中*p是指针变量的声明形式,表示定义了一个指向字符数据的指针变量,现在前面加*号,表示此时指针变量p指向“一个字符指针变量”,即p是一个“指向指针的指针”。 (有点混乱,慢慢理解)

接下来,对双指针p的相关操作与对指针数组名的操作是等价的,这里不再赘述。

空指针和野指针

ANSI标准增加了一个void指针类型,它可以定义指针变量,但没有指定它指向哪种类型的数据。

它可用于指向数据的抽象类型。 当将其值分配给另一个指针变量时,必须对其进行强制转换以使其适合所分配变量的类型,例如:

char *p1;
void *p2;
// ...
p1 = (char *)p2;

还可以使用(void *)p1将p1的值转换为void *类型,例如:p2 = (void *)p1;,也可以将函数的返回值定义为void *类型,例如例子:

void * func(char ch1, char ch2);

表示函数func返回一个地址,该地址指向“空类型”。 如果需要引用该地址,则需要根据情况进行类型转换,例如 char *p1 = (char *)func(ch1, ch2);

上面介绍的void *代表void指针类型,可以转换为其他指向类型。 在C语言中,它是双重定义的,(void *)0代表空指针常量。

如果p是一个指针变量,当它的值为void指针常量时,即p = (void *)0,那么此时p称为void指针,表示p指向一个空地址,即,地址 0 (不是常量 0),或指向 NULL。

根据定义,野指针是指向已删除对象或未申请访问受限内存区域的指针,即指向非法内存区域。 野指针也称为“杂散指针”或“悬空指针”。

对于一个指针p,当它所指向的对象被释放或回收时,但没有对该指针进行任何修改(例如不设置为NULL),使得该指针仍然指向已经被回收的内存地址,那么 p 就变成了野指针。 对指针 p 的后续操作可能会导致程序崩溃或产生不可预测的结果。

题外话

之前在网上看到过这样一段话:

我当时简单分析了一下,野指针一般指向的是一个被删除的对象,说明它曾经有过一个对象,现在只是被打散了,所以不会太惨。 如果你骂:“你TM就是一个没有对象的空指针!”,可能就更惨了,

总结

本文简单介绍了C语言中指针和数组的概念和关系,以及与之相关的一些知识点。 如有不当之处,敬请指出。 更多详细内容,强烈建议阅读谭浩强主编的《The C》和《C程序设计》教材,你会收获更多。

 
反对 0举报 0 收藏 0 打赏 0评论 0
 
更多>同类资讯
推荐图文
推荐资讯
点击排行
网站首页  |  关于我们  |  联系方式  |  使用协议  |  版权隐私  |  网站地图  |  排名推广  |  广告服务  |  积分换礼  |  网站留言  |  RSS订阅  |  违规举报
Powered By DESTOON