C语言指针二三事
# 一、起源
想学一下UNIX系统编程,所以把C重学一遍,C中指针关联甚多,是重点也是难点。
下面是我自己学习的总结,希望对你也有所帮助!
# 二、何为指针
# 1、C的特殊性
C是一门很特殊的语言,特殊的地方在于它对计算机是友好的,对程序员并不太友好。
常识上来说,我们是不需要知道一个变量在内存中的实际地址的,对于我们实现业务没有意义。但早期编写C的这帮计算机科学家都非常精通计算机原理,而且做的是底层研究,他们熟悉汇编,更熟悉计算机硬件原理,他们只是想做一个抽象层,编出一门语言来简化系统软件开发。
现在来看,对于学习JS、Java或是Python的人来说,C的编写过于复杂了,不够简洁。不过反过来看,我们也不能太苛刻,相比汇编语言来说,C已经好太多了。
# 2、初识指针
说到指针,它的定义就是:用于存储变量的内存地址。比如一个变量int num = 0;
来说,代码执行的时候必然会给他分配内存,如果你是一个高端玩家,想要知道num储存在内存具体的哪块地址上,那么可以用&num
来获得实际地址。
执行代码:
int num = 10;
printf("%d %p", num, &num);
执行结果大致为:
10 0060FF0C
那么获得一个内存地址有什么用呢?
我们先来看它在应用方面来讲最大的作用。
# 3、函数中变更变量的值
如果要实现两个int值互换,那么非常简单:
int num1 = 10;
int num2 = 20;
int temp = num2;
num2 = num1;
num1 = temp;
printf("%d %d",num1,num2);
如果要实现一个interchange
函数呢,你可能想到:
void interchange(int num1, int num2){
int temp = num2;
num2 = num1;
num1 = temp;
}
在main函数中调用:
int main(void){
int num1 = 10;
int num2 = 20;
interchange(num1,num2);
printf("%d %d",num1,num2);
}
打印出来的结果很有可能不符合预期哦,实际上num1和num2并没有进行交换。
这是怎么回事儿呢?这是由C语言的特性所决定的,num1和num2实际上只传递了值,传递了副本,所以可以这么看:在interchange()函数中的num1和num2已经是独立的值,已经和main()中的两个数没啥关系了,他们怎么改变也不会影响到main()中的num1和num2了。
说到这里,我们遇到了第一个问题,怎么解决呢?需要指针登场了!
# 4、指针的基础知识
先看下指针的基础知识,然后我们可以用这些知识来解决上述问题。
先有一个表达式:int num = 10;
,指针的知识:
- 获得一个变量的指针:
&num
- 指针声明:
int * pnum = &num
- 解指针:
*pnum = num
这里容易让人迷惑的地方是指针声明和解指针用的都是*
号。
简单来说,&num
也是一个类型,是一个什么类型呢?指针类型。那么如何声明指针类型:int * pnum;
,pnum
就是一个指针类型。
pnum
是一个指针,那么我想获取它的实际表达值怎么办?也就是怎么解这个指针呢?也用到*
号。所以*pnum
其实和num
是相等的。
重要提示:指针的大小取决于系统架构,在32位系统中是4字节,在64位系统中是8字节,与指向的数据类型无关。
# 5、利用指针解决问题
所以改良后的interchange()
函数如下:
void interchange(int * num1, int * num2){
int temp = *num2;
*num2 = *num1;
*num1 = temp;
}
调用:
interchange(&num1, &num2);
interchange()
函数用了指针和解指针的方式巧妙的改变了num1
和num2
变量的值。
# 6、指针安全问题
在使用指针时,需要特别注意以下安全问题:
空指针(NULL Pointer)
int *p = NULL; // 空指针,指向地址0
// 使用前必须检查
if (p != NULL) {
*p = 10; // 安全
}
野指针(Wild Pointer)
int *p; // 未初始化的指针,指向随机地址
*p = 10; // 危险!可能导致程序崩溃
悬垂指针(Dangling Pointer)
int *p = (int *)malloc(sizeof(int));
free(p); // 释放内存
*p = 10; // 危险!p现在是悬垂指针
p = NULL; // 好习惯:释放后置为NULL
# 三、数组与指针
定义一个数组,非常简单:
int powers[5] = {3,4,5,6,7};
下面开始引入数组与指针的关系了。
主要的规则有以下几点:
- 数组名在大多数表达式中会隐式转换为指向数组首元素的指针。也就是说
*powers
等于powers[0]
- 指针+1,则指针的值递增它所指向类型的大小。也就是说
powers+1
等于&powers[1]
- 注意:数组名是数组的标识符,不是变量,不能进行++操作。但是可以将数组名赋值给指针变量,然后对指针变量进行++操作
int powers[5] = {3,4,5,6,7};
// powers++; // 错误!数组名是常量
int *p = powers; // 正确
p++; // 正确,p现在指向powers[1]
多举几个等式的例子,感受一下:
powers = &powers[0]
powers + 2 = &powers[2]
*(powers + 2) = powers[2]
# 1、函数形参、指针与数组
数组可以理解为是一种特殊的指针,所以下面的四种函数原型是等价的:
int sum(int *ar, int n);
int sum(int *,int);
int sum(int ar[], int n);
int sum(int [],int);
# 2、指针与多维数组
先看一个多维数组的定义:
int zippo[4][2];
对这个多维数组进行分析:
zippo[0]
是一个占用一个int大小对象的地址,而zippo
是一个占用两个int大小对象的地址。它们两个都开始于同一个地址,所以zippo
和zippo[0]
的值相同[]
运算符本质上是指针运算的语法糖,a[i]
等价于*(a+i)
。对于多维数组,*zippo
等于zippo[0]
,**zippo
等于zippo[0][0]
请说明以下两个表达式的不同:
int (* pz)[2]; //1
int * pax[2]; //2
对于表达式1来说,int (* pz)[2]
声明了一个数组指针,即pz是一个指针,指向包含2个int元素的数组。
对于表达式2来说,int * pax[2]
声明了一个指针数组,即pax是一个数组,包含2个int类型的指针。
简单记忆:
int (* pz)[2]
- 括号优先,pz先和*结合,是指针,指向int[2]数组int * pax[2]
- []优先级高于*,pax先和[2]结合,是数组,元素是int*
# 3、多维数组与函数
如果多维数组作为参数来传递,那么函数原型声明上也有要注意的一些地方,先来看正确的声明方式:
int sum(int (* ar)[4], int rows);
int sum(int ar[][4], int rows);
再来看错误的方式:
int sum(int ar[][], int rows);
为什么下面是错的呢,是因为编译器会把数组表示法转换为指针表示法,那么就必须知道ar所指向的对象大小。
# 四、字符串与指针
在C中,字符串是以空字符(\0)结尾的char类型数组。
那么我们很容易想到他的定义:
char mesg[12] = "hello world"; // 注意:需要12个字符(11个字符+1个'\0')
char * pmesg = "hello world";
根据前面数组的内容,这两种定义都是ok的。
那么他们有什么异同呢?还是说使用的时候任何情况下都可以看成是含义相同的?
关键区别:
存储位置不同:
char mesg[]
:在栈上分配内存,内容可以修改char *pmesg
:指向存储在只读数据段的字符串字面量,内容不可修改
可修改性:
char mesg[12] = "hello world";
mesg[0] = 'H'; // 正确,可以修改
char *pmesg = "hello world";
pmesg[0] = 'H'; // 错误!运行时会崩溃,试图修改只读内存
- 使用建议:
- 需要修改字符串内容时,使用字符数组
- 只读字符串时,使用
const char *
更安全 scanf()
必须使用字符数组,因为需要写入内容
# 五、结构与指针
C中的结构如:
struct node{
int num;
};
跟Java中的Class相似。
这里要讲到结构指针,作用和前面的函数中变更变量的值差不多。
先思考一个问题,如果一个结构作为入参传入一个函数中,函数中修改结构的值会影响到main()函数中结构的值么?
可能答案你已经猜出来了,是不能的。这里还是要借助指针来实现:
void struct_demo(){
struct node tn;
tn.num = 1;
struct_demo_swap(&tn);
printf("%d",tn.num);
}
void struct_demo_swap(struct node * temp){
temp->num = 10;
}
打印出来的结果是tn.num=10
。
注意,这里对于指针访问结构成员有两种方式:(*temp).num
或temp->num
# 六、函数与指针
C语言支持函数指针,可以将函数作为参数传递。注意:这与JavaScript的闭包或Java 8的Lambda表达式不同,C的函数指针不能捕获外部变量(没有闭包特性)。
在C中,函数也有地址,指向函数的指针中存储着函数代码的起始处的地址。
一个规则:函数名可以用于表示函数的地址。看代码:
void ToUpper(char *);
void (*pf)(char *);
pf = ToUpper;
可以看到第二行声明了一个函数指针叫pf
,这个函数指针定义了返回值和形参列表,只要是和他结构一样的,都可以赋值给他。这里pf = ToUpper;
没有什么问题。
这样的规则会存在一些小问题:*pf
实际上表示ToUpper
函数,而pf和函数名又可以互换,所以(*pf)("abc")
和pf("abc")
、ToUpper("abc")
又都是等价的。
下面看一个函数指针最常用的用法,就是作为入参:
void show(void (* fp)(char *), char * str);
实际应用示例 - qsort函数:
#include <stdio.h>
#include <stdlib.h>
// 比较函数,用于qsort
int compare(const void *a, const void *b) {
return (*(int*)a - *(int*)b);
}
int main() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int n = sizeof(arr)/sizeof(arr[0]);
// qsort使用函数指针作为参数
qsort(arr, n, sizeof(int), compare);
printf("排序后的数组: ");
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
return 0;
}
# 七、const指针
const与指针的结合使用是C语言中的重要概念:
1. 指向const的指针(底层const)
const int *p; // p可以改变,但不能通过p修改所指向的值
int const *p; // 同上,两种写法等价
2. const指针(顶层const)
int * const p = # // p不能改变,但可以通过p修改所指向的值
3. 指向const的const指针
const int * const p = # // p不能改变,也不能通过p修改所指向的值
记忆技巧:const修饰其左边的内容,如果左边没有内容,则修饰右边。
关于指针的知识就总结完了,希望你有所收获!