C中的指针

本文详细介绍了C语言中的指针,包括指针的概念、用途,以及如何用于形参、函数返回、处理数组和字符串。此外,还讨论了指针在处理函数、数组和字符串时的高级应用,如指针数组和指向函数的指针。文章强调了指针在C语言中的重要性,并指出其灵活性和潜在的复杂性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录


指针

指针是C语言的特色,为什么C语言会有指针?因为C语言是底层语言,底层语言时时刻刻不可避免的会与内存打交道,指针带来了操作内存的强大能力和编写代码的便利性,缺点是大大增加了代码的复杂性和阅读难度,且增大了bug出现的机率,从某些方面来说非常考验程序员的功底。所谓指针就是内存地址,一个内存单元为1字节,但是单元编号却很大,64位程序用8个字节编号,32位程序用4字节编号,4字节最大能表示4294967295,这就是为什么32位操作系统最大支持4G内存的原因,而8字节能表示的地址远远超过目前硬件的制造水平。内存控制器通过地址编号来存取内存单元,变量名在编译的时候会被翻译为内存地址,因此通过变量名存取称为直接存取,我们也可以将内存地址作为数据保存起来,记录这个编号的内存开销可能比其数据本身还要大,例如记录一个字符需要1字节,记录这个字符内存的地址可能需要8字节。程序启动后会向操作系统申请内存开销,变量所记录的地址,函数的定义等都被放入静态内存空间中,而函数运行时所需的内存开销,用户申请的空间属于动态内存空间。当我们保存了一个变量的地址后,除了通过变量名访问,还可以通过保存的地址来访问,也就是通过指针访问,这种方式称为间接访问。很明显,间接访问比直接访问需要的开销大,但带来的好处是增强了代码功能和灵活性。下面看看如何获取一个变量的内存地址,之前我们知道通过变量名访问可以获得变量储存的内容,如果要获取变量表示的内存地址,可以通过&符号来获取,例如:
printf("&a=%p,&f=%p\n",&a,&f);
32位应用程序用%d输出就够了,64位应用程序需要使用%ld输出,还可以选择一个专为输出指针定义的格式符%p,它会自动处理32位和64位地址并以固定长度输出。除了输出可见的内存地址,我们还可以定义一个指针变量,将这个地址作为它的值从而记录该地址,例如:
int p = &a;
其中p是变量名,int
是变量类型,也可以写作int* p,定义之后通过指针p就可以间接获取变量a的值,例如:
printf(“%d”,p);
使用
p间接获取变量a的值称为指针运算,也可以通过p修改变量a的值,如p=100。指针运算时必须指定其数据类型,否则编译器不知道如何取值,进行指针位移时也必须确定指针类型,否则编译器不知道要移动多少字节。初始化指针变量时如果没有确定值,应该初始化为NULL,NULL是C语言中定义的常量,值为0,但是NULL不等同于0,它的类型是void*。NULL是一个安全的值,标准库中很多函数都会判断指针是否为空,如果为空不会执行任何操作,在进程的虚拟地址空间中有一段内存区域被称为保留区,这个区域不存储有效数据,也不能被用户程序访问,NULL 指向这块区域很容易检测到,如果你不将指针初始化为NULL,它可能指向一个未知的内存地址,对未知地址操作是非常危险的。

如果指针只能做一些基本数据类型的内存存取操作可以说没有什么用处,还不如直接用变量名存取减少开销,指针彰显威力的地方在于处理形参、数组、函数等高级问题,可以说C语言的灵活性大多来自于指针操作,如果没有指针C语言就是一个功能有限且非常死板的语言,做不了底层工作,有了指针就不一样了,因为可以直接操作内存,指针的用途可以说是讲不完的,但是一些基本用途是必须掌握的。

用于形参

形参是指针最基本的应用,实参按值传递,按值传递是数据克隆的过程,克隆后得副本数据,修改副本数据对原始数据不会产生影响,优点是安全,缺点是耗费资源多。形参传递数据地址,通过传递地址操作数据只会对同一份数据进行操作,优点是性能高,没有数据克隆这一过程,缺点是可能破坏原始数据,因此常常用常量指针来保护原始数据。

函数返回指针

既然指针可以用作形参,当然也可以用于函数的返回值,函数返回指针时书写格式如下:

int * fun(int a, int b)
{
	int* p;
	return p;
}

int* 表示返回类型为整数指针,由于栈中的数据是临时的,要避免返回函数内部创建的数据地址,但可以返回其它地址,例如传入的形参或堆上的地址,下面代码在函数中创建一个新的数组,然后返回它。

#include<stdio.h>
#include<stdlib.h>

int arr1[2]={1,2};
int arr2[3]={3,4,5};

int *arrCat(int arr1[2],int arr2[3])
{
	int *arr=malloc(5);
	int i=0;
	for(i=0;i<5;i++) *(arr+i)=*((i<2?arr1:arr2)+(i<2?i:i-2));
	return arr;
}

int main()
{
	int *arr=arrCat(arr1,arr2);
	for (int i = 0; i < 5; i++) printf("%d ",arr[i]);
	free(arr);
}

函数arrCat连接两个传入的数组,在内部动态创建一个新数组并返回它,在main()中输出这个新数组后释放动态申请的空间。

处理数组

有了指针的概念后我们再来看数组概念就更明确了,数组名实际上是一个常量指针,不允许修改它的指向,但可以修改其指向数据的内容,它的类型为数据集合,但支持自动向下转型,它的值既是数据集合的首地址,也是首元素的地址,因为值相同才体现了自动转型带来的便利。在使用arr[i]时,arr被当作数据集合进行下标运算,而对于arr+i或直接使用arr来说arr被降级为元素类型。sizeof()在获取长度时会判断其类型,在sizeof(arr)被当作数据集合。将数组名作为地址赋值给指针和按形参传递都属于降级操作,自动转型一方面便于通过指针操作数组元素,另一方面也方便了形参传递。由于数组名既是数据集合也是首元素的地址,因此数组arr和&arr[0]地址相同,你可以定义一个指针变量记录这个地址,例如int *p=arr或int *p=&a[0],当访问数组元素时,除了使用下标,C标准还提供了通过指针位移存取元素的专用方法,称为指针法,具体为:

  • 由于a[10]表示一个数组,a可以降级表示首元素地址,指针法规定a+1表示第二个元素地址,a+2表示第三个元素地址,以此类推…,不难看出a与a[0]等价,(a+1)与a[1]等价。
  • 如果指针变量p指向a,还可以进行自增或自减运算,例如p++, p–, p+=3。
  • p2-p1可以求出两个数组元素之间的位置差。
  • 指针运算符与自增自减运算符优先级相同,自运算符的顺序是由右向左,因此p++表示在移动元素之前取值,*++p表示在移动元素后取值,++*p则表示取值后再自增。
  • 支持比较运算,p<p+1返回true,p+1==a+1返回true,这就给循环带来了便利。

相比下标运算,指针运算提供了更高的编码效率和更简洁的代码,下面是一个使用指针遍历数组的例子:

#include<stdio.h>

int a[5]={1,2,3,4,5};

int main(void)
{
	int *p=a;
	while(p<a+5) printf("%d\n",*p++);
	return 0;
}

实际上下标在编译时也会转化为*(a+i)的形式,直接用指针法就少了这个编译步骤,另外指针法从逻辑上讲是一种相对地址操作,可以让指针在数组元素间游历,这在处理某些问题上更加敏捷,而下标法是绝对地址操作,随时需要知道当前元素索引,在处理多维数组时逻辑比较清晰,在指针法中求当前元素的索引也很简单,用p-a即可得到。

现在我们来讲一个容易忽略的问题,我们知道使用sizeof()可以求出数组或字符串的长度,而对于常量字符串、形参以及用malloc()声明的空间能不能不能用sizeof()求出长度呢?不能,因为字符串和malloc()返回结果没有对应的数据集合类型,只能用变量或常量记录首地址,测试代码如下:

#include <stdio.h>
#include <string.h>
#include<stdlib.h>

int main()
{
	int arr[3] = { 1,2,3 };
	int* parr = arr;
	printf("%d,%d\n", sizeof(arr), sizeof(parr));

	char str[] = "abc";
	char* pstr = str;
	printf("%d,%d\n", sizeof(str), sizeof(pstr));

	char* p = malloc(20);
	printf("%d\n", sizeof(p));
	free(p);

	return(0);
}

处理字符串

由于C没有专门的字符串类型,用数组创建字符串总是显得别扭,有了指针后这个问题得到缓解,使用指针创建一个字符串方法如下:
#include <stdio.h>

char* c=“abcde”;

int main(void)
{
puts©;
c=”hijkl”;
puts©;
return 0;
}
但是这种创建方式和字符数组有着本质的区别,“abcde"实际上是字符串的字面值,它是一个没有名字的匿名字符数组,而且这个字面值数组的储存方式也不一样,只要编译器发现代码中有字符串的字面值,它将被当作常量放置于内存的静态区,静态区放置代码的定义和常量,常量和代码定义一样会增加可执行程序的大小。无论在栈中还是堆上创建了字符串的字面值,在编译时都会将它纳入到静态区,在静态区相同的字符串常量只会创建一次,在使用时返回该字符串地址而不是再次创建它。例如在任意代码位置中将"abcde"赋值给指针c,在程序编译时会在静态区创建字符数组常量"abcde”,c指向这个静态区数据的地址,当函数执行完毕或堆空间被释放"abcde"也不会消失,即使变更c的指向,这个地址也不会丢失,下次使用"abcde"仍会返回这个地址。由于常量是不允许修改的,尝试修改字符串字面值内容会导致编译器报错,这是和普通字符数组最大的区别,要修改变量c只能让它指向另外一个字符串。对比普通字符数组和字符串字面值,从指针类型上看,一个是常量指针,一个是指针常量,一个只能修改内容,一个只能修改引用,刚好互为补充。在工作中只需截取字符串内容时可以创建字符串字面值,如果需要修改内容就必须创建字符数组,请看下面代码:

#include <stdio.h>
#include <string.h>

int main(void)
{
	char* str = "abcdefg";
	char str1[10] = "";
	char str2[10] = "";
	char str3[10] = "";

	char arr[] = "abcdefg";
	arr[0] = '\0';
	puts(arr);//arr的内容被修改
	puts(str);//str的内容不变

	//截取从第4个字符开始到末尾的内容
	puts(str + 4);
	//截取前4个字符
	strncpy(str1, str, 4);
	puts(str1);
	//截取从第2个字符开始长度为3的内容
	strncpy(str2, str + 2, 3);
	puts(str2);
	//截取从第2个字符开始到末尾倒数第2个字符串前的内容
	strncpy(str3, str + 2, strlen(str) - 2);
	puts(str3);
}

我们先用两种方式创建相同的字符串"abcdefg",修改字符数组arr的内容后进行测试,发现str并不受影响,而后我们开始截取字符串,str+4表示从第4个字符开始的字符串常量,如果截取的内容今后需要修改,则应该用strcpy()来创建副本,截取中间的字符串需要结合指针和strncpy()函数,使用负值索引需要结合strlen()函数,以此类推还可以写出删除中间内容的句子,无论怎样使用字符串函数都比编写一个循环然后一个个复制字符效率高。使用数组名时,不能使用arr++这样的自增运算,但是可以使用str++来进行指针位移,如果想两者兼顾,可以创建arr数组然后用指针p指向arr。

指向函数的指针

很多高级语言都可以将函数作为一个对象进行传递,将函数名赋值给变量或者作为参数进行传递可以创造很强的功能,例如将函数放入数组中轮询可以进行密集计算,将函数作为参数传入可以执行回调。C语言虽然不是面向对象的语言,但是有了指针后可以实现这些功能,前提是要获取函数的内存地址。我们知道函数在运行时会在栈上申请动态内存空间,但这个内存空间不是这里需要的函数地址,这里需要的是函数定义的代码块地址,函数的定义保存在静态空间中,无论函数是否执行这段空间都是存在的,这段可执行的代码有个起始地址,也称为函数的入口地址,函数名就代表这个入口地址。再来看看函数指针,假设我们定义了一个函数test,如下:

int test(int x,int y)
{
	return x+y;
}

那么函数test的类型为int(int,int),值为函数的入口地址,test是一个常量,既不允许修改它的指向也不能修改函数代码块的内容,如果我们需要定义一个引用该函数的指针变量,这个指针类型必须为int(int,int),例如:
int (*p)(int,int);
这里(*p)是一个整体,必须用括号括起来,因为按照优先级必须是从左到右,否则就成了函数引用声明,下面用一段代码测试函数指针:

#include <stdio.h>

int test(int x, int y)
{
	return x + y;
}

int (*p)(int, int) = test;

int main(void)
{
	printf("%p\n", test);
	printf("%p\n", p);
	printf("%d\n",(*p)(1, 2));
	printf("%d\n", p(1, 2));
	printf("%p\n", test);
	//printf("%p\n", p);
	//printf("%d,%d\n",sizeof(test),sizeof(p));
	return 0;
}

这个例子声明了一个函数类型的指针p指向函数test,运行结果表明,函数入口地址在函数执行之前已经存在,因为函数定义保存在内存静态区,通过函数名、指针都可以输出这个地址并且这个地址在函数执行完毕后仍然不变。调用函数时,先从静态区取出函数定义,然后在栈上创建动态空间,接着执行函数返回结果,当需要访问局部变量时会自动到栈区取,函数调用完后栈空间被释放,但函数代码块不会有任何变化,在递归调用中,虽然在栈上分配的执行空间不同,但代码定义块是一样的。有了这个函数指针p后,p可以指向参数类型和返回类型相同的任何函数,这些函数通常是功能相似算法不同,如果要通过指针执行一个函数,指针运算格式为(*p)(2,2),声明的格式和调用格式一样,先进行指针运算获取函数定义后才能调用函数。
函数名和数组名不一样,它是一段定义块的起始地址,没有自动降级这样的操作,当使用sizeof(test)求值时,求出的应该是函数定义块的大小,可惜不是每个编译器都能获取成功的,大部分编译器用sizeof()求函数定义块大小时要么运行报错,要么返回首地址,而用sizeof§求出的是指针大小。指针p指向test()的入口地址,但p并不一定代表函数test,它还可以指向参数类型和返回值类型相同的其它函数,这就实现p作为形参的目的。下面写一个函数,这个函数能够传入其他的函数并且实现回调:

#include<stdio.h>
#include<stdlib.h>

void callback(void (*p)())
{
	(*p)();
}

void test1()
{
	puts("test1");
}

void test2()
{
	puts("test2");
}

int main()
{
	callback(test1);
	callback(test2);
}

如果我们进行代码测试,会发现(*p)()和p()都能执行函数,那么到底哪个对呢?其实这里面有历史遗留的问题,贝尔实验室的C和UNIX的开发者采用第1种形式,而伯克利的UNIX推广者却采用第2种形式。K&R C不允许第2种形式,因为C标准要求指针的声明格式和运算格式必须一致,但p()也不会产生歧义,为了与现有代码兼容,ANSI C认为这两种形都对,后续的标准也延续了这种矛盾的和谐,第一种方式用起来更加标准,第二种方式在加载动态库函数时用起来更加顺手。


## 多维数组指针

对于一个二维数组,每个元素都是一个一维数组,而一维数组的数组名又是首元素地址,因此二维数组中每个元素都是一维数组首元素的地址。如果把二维数组看成行和列,在二维数组a[3][2]中,a[0]表示0行0列元素的地址,a[1]表示1行0列元素的地址,a[0]+1表示0行1列元素的地址,a[2]+1表示2行1列元素的地址,*(a[2]+1)相当于a[2][1],下标法在编译时会转换成指针法。如果我们想要创建一个指针p指向二维数组,这个指针如何定义呢?经过前面对数组类型的讲解你已经知道,对于a[3][2]来说,一维数组类型为int [2],同样二维数组的类型为int [3][2],指向二维数组的指针需要降级为其元素的类型,即int [2],因此二维数组的指针定义格式为:int (*p)[2],(*p)是一个整体,顺序为从左到右,表示先从二维数组中取出一维数组,然后再获取一维数组的元素,声明顺序和执行顺序相同,否则按照从右到左的顺序就成了指针数组,下面定义一个指针来遍历二维数组:

```c
#include<stdio.h>

int a[3][2]={{1,1},{2,2},{3,3}};

int main(void)
{
	int (*p)[2]=a; //行地址
	int *q; //列地址
	do
	{
		q=*p;
		do printf("%d\n",*q);while(++q<*p+2);
	}while(++p<a+3);
}

在这个例子中p指向行地址,q指向列地址,*p表示该行首列元素地址, *q表示p行q列元素的值。可以看到使用指针法处理二维数组比下标法要精简,但不如下标明确。掌握了二维数组指针后就不难写出声明指向3维数组的指针格式为int (*p)[3][2],四维数组指针格式为int (*p)[3][2][1],以此类推。

指针数组

有时后我们需要将指针放入数组中,例如将字符串放入一个数组中储存,或者将函数放入数组中进行密集调用,此时需要创建一个指针数组,那么这个指针数组该如何声明?指向这个指针数组的指针又怎么表示呢?对于数组来说,数组名和下标是一个整体,其类型表示数组元素的类型,例如通过数组名创建一个指针数组:
int s[3]={…};
这是一个长度为3的指针数组,s是数组名,s[3]是一个整体,元素类型为int

前面讲过指向数组的指针是降级为元素类型后的指针,指针数组每个元素都是一个指针,再对该类型取地址,指向指针数组的指针就成了指针的指针,对于数组s来说,元素类型为int*,因此指向数组的指针为int **p,如下:
int **p=s;
数组名既表示数据集合,在运算时也自动降级为指针的指针。当对指针求值时,*p获取数组元素,**p再对数组元素进行指针运算。

字符串数组

如果数组中放置的是字符串,数组类型为char *strArr[],指向数组的指针为char **p,下例将字符串放入一个指针数组中,然后用指针来遍历:
#include<stdio.h>

char *strArr[]={“Chinese”,“America”,“Australia”}; //创建一个字符串数组,让编译器自动算出数组长度

char **p=strArr; //用于遍历字符串数组的指针

int main(void)
{
	while(p<strArr+3) puts(*p++);
}

这里p或strArr[1]并不是字符串常量的真实名称,常量字符串是没有名称的,我们可以通过strArr[0]=”abc”或p=”abc”来修改数组元素的值,但不能修改字符串的内容。

函数数组

如果数组元素为函数,函数类型为void (*fun)(),函数数组为void (*funArr[])(),函数名和下标是一个整体。数组元素类型为void (*p)(),因此指向数组的指针为为void (**p)()。下例将几个功能类似的函数放入数组中,然后遍历数组执行:

#include<stdio.h>

void fun1()
{
	puts("fun1");
}

void fun2()
{
	puts("fun2");
}

void fun3()
{
	puts("fun3");
}

void (*funArr[3])()={fun1,fun2,fun3};	//将指针数组和函数类型相结合,定义一个指向包含函数的一维数组的指针

void (**p)()=funArr;	//p是指向数组名的指针 

int main(void)
{
	while(p<funArr+3) (**p++)();//调用函数,调用形式和声明形式相同。
	return 0;
}

指针数组比普通指针难度要高一些,因为数组元素可以是各种类型,要将数组下标与各种类型的指针结合起来书写有一定难度,但只要记住通过数组名声明数组时数组名和下标是一个整体就不难写出,定义数组指针时只要记住指向数组的指针是先降级为数组元素类型再取地址就不难写出。多维数组其实也是指针数组,但声明多维数组有固定的下标语法如arr[3][2][1],无需套用该法则,指向多维数组的指针依然遵守降级取址的法则。

更复杂的指针

实际工作中还可能遇到更复杂的指针,各种类型的指针叠加起来确实容易让人眼花缭乱,但有一点不变,就是指针声明格式和求值的格式是一致的,换句话说编译器解析指针声明和求值的顺序是一致的,当我们看到一个复杂指针声明时,首先找到指针变量,然后按照求值的顺序一层层分析,就像阅读表达式一样,再复杂的指针都可以一步步搞清楚。我们先用这种方法回顾一下已学过的指针类型:
对于一维数组指针int (*p)[3],先进行指针运算还原一维数组后再通过下标求值
对于指针数组int p[3]是先通过下标获得数组元素后,再对数组元素进行指针运算求元素值
对于函数指针int (p)(int,int)是先通过p获取函数定义,然后再调用函数
对于指针的指针int **P,先通过
p获取二级级指针变量的值,再通过**P获取一级指针变量的值
对于函数数组void (*funArr[3])(),先获取元素值取得函数入口地址,然后调用函数。
上面的方法是通过求值运算步骤来解析指针,还有一种变通的方法,那就是求类型。求类型就是求指针运算后返回值的类型,步骤是先找到指针变量,然后逐步求出它的类型,在这个过程中会经历和求值法一样的步骤,例如对于一个普通指针int p,指针变量为p,类型为int,因此是一个整形指针,再看看其他类型:
对于int (*p)[3],指针变量为p,p是一个指针,但还不知道类型,我们用v(value)来代替p的值,跳出括号结果为int v[3],这个类型为一维数组,因此p是一维数组的指针。
对于int *p[3],指针变量为p,按照优先级p[3]表示一个数组,但还不知道数组元素类型,用v代替p[3]的值进行第二步推演,结果为int *v,因此一维数组的元素类型为int *,是一个指针数组。
对于函数指针int (*p)(int, int),指针变量为p,p是一个指针但还不知道类型,我们用v代替p,跳出括号,结果为int v(int,int),这个类型为函数,因此p为函数指针。
对于int **P,指针变量为p,p是一个指针,但还不知道类型,用v代替p,结果为int *v,指针类型又是一个整形指针,因此p是指针的指针。
求类型法和求值法难度相同,上面几种指针是最常见的指针组合,记住上面这些基本指针类型后,结合求类型法分析复杂的指针会更快,下面我一起来看几个指针:
char ( c[10])(int p);
这个表达式有两个指针,按照运算优先级来分析,先找到指针变量c,括号里面的
c[10]是一个指针数组,但还不知道元素类型,同v代替括号内容,跳出括号得到char *v(int *p),这是一个函数,它的参数是指针,返回值也是指针,因此指针数组元素的类型为函数。

int (((*pfunc)(int *))[5])(int )
按照表达式执行顺序,首先找到指针变量pfunc,一眼可以看出
(*pfunc)(int )是一个函数指针,它的参数为整形指针,返回值也是一个指针但类型未知,继续寻找返回值类型,用v代替(pfunc)(int ),外层括号为v[5],这是一个指针数组,元素类型未知,继续用v代替v[5],结果为int (*v)(int *),类型为函数,参数是指针,返回值为整数。综合起来就是有一个指针指向一个函数,这个函数的返回类型为指针数组,指针数组的元素也是函数。当我们声明指针时也是按这样的顺序,即先判断指针的整体类型,再分析细节类型。例如指针p整体为函数,先写出一个函数指针,再完善它的参数和返回值。如果p为数组指针,先写出一个数组指针,再完善元素的类型。从格式上看指针整体类型包裹在最里面,解析顺序是从里到外的。如果不能一次到位,可以先写出前面讲的基本类型然后再进行演变,最后编写代码进行测试。

宽松的指针类型转换

C语言对数据类型的要求并不严格,例如char经常当作单字节整形来使用,我们也常常将一个int指定给一个char,但对于其它数据类型就不一定了。对于基本数据类型的转换,如果有精度损失会发出警告,对于结构体类型则是要求严格匹配的。然而对于指针类型来说几乎没有限制,因为所有指针返回的都是内存地址,不会有信息损失,只是作指针运算的时候要告诉编译器如何按照类型取值。C标准规定void*是一个无类型指针,编译器支持它和其它类型之间自动转换,其实现在先进的编译器也支持其它类型之间的自动转换,例如:

#include <stdio.h>
#include<stdlib.h>

void fun()
{
	puts("fun");
}

int main(void)
{
	int a = 1;
	float b = 2.0;
	char* p = "abc";
	int* pa = &b;
	float* pb = pa;
	pb = p;

	void (*f)() = fun;
	f = p;

	typedef int(*myFun)(int, int);
	myFun f1 = f;
	p = f1;
	f1 = pa;
}

这段代码在gcc中到处报错,但在VS中全部通过,如果在gcc中使用强制转型也可能顺利编译。宽松的指针类型转换提供了通用类型技巧,例如使用void 接受任何类型的参数,将堆上申请的空间存放任何数据类型,在加载动态链接库时可以使用void指针加载任意类型的变量和函数,对于VS来说除了基本数据类型之间可以自动转换,其它指针类型之间转换不一定能通过编译。

指针总结

指针可以说是C语言的精华,C语言的高级功能都是依赖指针完成的,付出的代价就是代码变得难读,需要扎实的功底。使用c或c++编写的虚拟机承载的高级语言特性,例如形参、回调函数、函数数组在底层也是通过指针实现的。其实指针的概念并不难,难的是要搞清楚各种指针的定义格式,还要结合这些格式阅读和编写自己的指针,为了增加代码的可读性,后面会讲解使用typedef来定义类型的别名。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值