C语言中的指针

2 阅读5分钟

我们今天介绍一下C语言的核心概念指针,指针和内存中的地址是一个概念。

指针变量

变量创建的本质是在内存中开辟空间并存储它,而指针变量本质上也是变量,但存放的是变量的地址。

int a = 0;
int* p = (int*)NULL;
p = &a;
int** pp = &p;

int* p:定义去掉变量名即为类型int*,int*的*声明p为指针变量即存放变量的地址,而int表示存放的变量是整型,也称为p指向的变量是整型。

pp是二级指针,类型为int**,本质上为(int*)*,存放指针变量的地址。

任何局部变量创建都需要初始化,否则值为随机垃圾值,读取行为未定义。NULL是空指针,在C语言中即(void*)0,void*是泛型指针即可接受任意类型的地址,会隐式转换成任意指针类型。NULL表示该指针不指向任何有效地址。事实上操作系统会把虚拟地址0附近一整页内存设置为禁止访问。在不需要p指向哪个变量时,可为指针变量初始化为NULL。字面量0用于指针时也表示NULL,但容易造成歧义,不推荐。

&+变量名表示取变量的首个较低地址,解引用*与之相反,把对应地址的变量取出来。根据变量的类型决定取类型大小个字节,故对void*类型无法解引用。注意指针变量大小跟指向的类型毫无关系,因为存放的是地址。

指针的运算

若p为指针变量,p+1表示以p指向变量的类型的大小为一个单位跳过一个单位,同样对void*类型无定义。因此两个指针的差的绝对值在良定义(同类型且指向有意义的内存空间)时表示两个指针之间元素的个数。地址本质上是数字编码,因此可进行关系运算。

数组与指针的关系

数组名arr一般表示首元素arr[0]的地址(除了sizeof(数组名)和&数组名),如果sizeof后面也是首元素地址,将无法计算数组的大小,&数组名是整个数组的地址,&数组名+1跳过整个数组大小,方便二维数组下标的引用。注意数组下标从小到大,地址连续增加。

下标引用操作符arr[i]表示*(arr+i),若arr为二维数组,一维数组arr[0],arr[1],…为元素连续存储,故arr[i][j]即*(*(arr+i)+j),arr+i即&arr[0]+i*sizeof(arr[0])。

函数的传值调用

函数的参数传递在C语言中为传值调用(因为直观),即形参为实参的副本,有独立的内存空间,因此在函数内部修改变量不会影响调用的实参,因此必须传入指针变量,通过指针变量解引用间接修改变量,传递地址是拷贝了地址,而非变量本身!若想修改指针变量,则需要传递二级指针变量,依此类推...

字符指针

char* p1 = "abc";
char* p2 = "abc";
printf("%c\n","abc"[1]);

字符指针p1指向"abc"的首元素a,注意"abc"放在内存的只读数据段,不能被修改,数组则不同,存储在栈区或被static修饰,存储在静态区,允许被修改,"abc"在非初始化的参与表达式运算时退化为首元素的地址,故"abc"[1]是合法的。p1,p2一般是同一个地址,但属于实现定义,并非强制规定。

数组指针和指针数组

int a = 0;
int arr[10] = { 0 };
int(*parr)[10] = &arr;
int* pa[] = { &a };

数组定义去掉数组名为数组类型,再去掉[]为每个数组元素的类型。int(*parr)[10]表示数组指针,加()是因为[]优先级比解引用*(注意这里*不是解引用的意思,而是声明这是个指针变量)高,故int*parr[10]即int*(parr[10]),即10个int*构成的数组。这里的[10]不能随意省略,否则指针不太好运算。

指针的应用:模拟库函数strlen

注意尽管我们在传递数组时一般需要数组元素个数(退化为指针),但strlen不需要,数组个数是为了防止越界,而字符串结束表示是'\0',ASCII码值为0。注意字符串会退化为首元素地址。

size_t my_strlen1(const char* p)
{
	size_t count = 0;
	while (*p)
	{
		count++;
		p++;
	}
	return count;
}

注意*p=='\0'时,恰好退出循环,字符相当于ASCII码值

若要求不创建临时变量,则可以采用递归的方式:

size_t my_strlen2(const char* p)
{
	if (*p == '\0')
		return 0;
	else
		return 1 + my_strlen2(p + 1);
}

指针的好处

有人可能感到疑惑,我直接修改变量不就行了,为什么需要通过指针间接访问呢?事实上如果数组或结构体很大,依然选择传递变量本身,可能会浪费空间,且数组下标是通过地址访问的,可以通过修改地址但不更改数据修改数据直接的逻辑关系。当然还有更多的好处,需要在对指针有更加深入的了解之后逐步体会。