内存和地址
内存其实就是一组有序字节组成的数组,数组中,每个字节大小固定,都是 8bit。对这些连续的字节从 0 开始进行编号,每个字节都有唯一的一个编号,这个编号就是内存地址。示意如下图:
指针变量保存的就是这些编号,也即内存地址。
地址与内容
我们只要知道内存地址,就可以访问这个地址的值,但是这种方法实在笨拙,于是便用变量名来代替地址:
名字与内存之间的关联仅仅只是编译器实现的,采用变量名的方式能够方便的记住地址,但是硬件仍然通过地址访问内存位置。
值和类型
考虑下面的32位值:
01100111011011000110111101100010
对于这些位的解释可以分为很多种:
类型 | 值 |
---|---|
1个32位数 | 1735159650 |
2个16位数 | 26476和28514 |
4个字符 | glob |
浮点数 | 1.116533e24 |
机器指令 | beg.+110和ble.+102 |
可见:不能简单地通过检查一个值的位来判断值的类型,而应该根据它的使用方式来判断。
使用指针的优势
在C语言中,指针的使用非常广泛,因为使用指针往往可以生成更高效、更紧凑的代码。总的来说,使用指针有如下好处:
- 指针的使用使得不同区域的代码可以轻易的共享内存数据,这样可以使程序更为快速高效。
- C语言中一些复杂的数据结构往往需要使用指针来构建,如链表、二叉树等。
- C语言是传值调用,而有些操作传值调用是无法完成的,如通过被调函数修改调用函数的对象,但是这种操作可以由指针来完成,而且并不违背传值调用。
取变量地址间接访问
对于一个操作数取地址使用单目操作符&
。
通过指针访问它所指向的地址的过程称为间接访问或解引用指针,执行间接访问的操作符是单目操作符*
。
声明一个指针
未初始化和非法指针
声明一个指针变量并不会自动分配任何内存。在对指针进行间接访问之前,指针必须进行初始化:
- 使它指向现有的内存
- 给它动态分配内存
指针的初始化实际上就是给指针一个合法的地址,让程序能够清楚地知道指针指向哪儿:
/* 方法1:使指针指向现有的内存 */
int x = 1;
int *p = &x; // 指针 p 被初始化,指向变量 x ,其中取地址符 & 用于产生操作数内存地址
/* 方法2:动态分配内存给指针 */
int *p;
p = (int *)malloc(sizeof(int) * 10); // malloc 函数用于动态分配内存
free(p); // free 函数用于释放一块已经分配的内存,常与 malloc 函数一起使用
果一个指针没有被初始化,那么程序就不知道它指向哪里。它可能指向一个非法地址,这时,程序会报错,在 Linux 上,错误类型是 Segmentation fault(core dumped)提醒我们段违例或内存错误。它也可能指向一个合法地址,实际上,这种情况更严重,你的程序或许能正常运行,但是这个没有被初始化的指针所指向的那个位置的值将会被修改,而你并无意去修改它。
NULL指针
NULL 指针是一个特殊的指针变量,表示不指向任何东西。可以通过给一个指针赋一个零值来生成一个 NULL 指针。
对一个NULL指针解引用操作是非法的,在对指针解引用之前必须确保它并非是NULL指针。
指针的运算
算术运算
C 指针的算术运算只限于两种形式:指针 +/- 整数 ,指针 - 指针。
指针 +/- 整数
可以对指针变量 p 进行 p++、p--、p + i 等操作,所得结果也是一个指针,只是指针所指向的内存地址相比于 p 所指的内存地址前进或者后退了 i 个操作数。
指针 - 指针
只有当两个指针都指向同一个数组中的元素时,才允许从一个指针减去另一个指针。
两个指针相减的结果的类型是 ptrdiff_t
,它是一种有符号整数类型。
减法运算的值是两个指针在内存中的距离(以数组元素的长度为单位,而不是以字节为单位),因为减法运算的结果将除以数组元素类型的长度。
关系运算
使用操作符<,<=,>,>=
对两个指针比较的前提是两个指针指向同一个数组中的元素,比较的结果是哪个指针指向数组更前或者更后的元素。
标准没有定义两个任意指针之间的比较会发生什么。
任意两个指针之间可以使用操作符!=、==
进行比较,判断它们是不是指向同一个地址。
指针表达式
假设:
char ch = 'a';
char* cp = &ch;
那么:
表达式 | 含义 |
---|---|
*cp |
作为左值,表示cp 指向的地址,也就是变量ch 的地址;作为右值,表示cp 所指向地址存放的内容,也就是'a' 。 |
*cp + 1 |
作为右值,(*cp + 1) 表示存放的内容加1,就是字符'a'+1 ,得到字符'b' ;作为左值,*cp 作为左值的意思是cp 指向的那个地址本身,由于*cp + 1 这个表达式的最终结果的存储位置并未清晰定义,所以它不是一个合法的左值。 |
*(cp + 1) |
作为右值,访问的是cp 指向的位置的下一个位置的值;作为左值的话,就是cp+1 这个指针对应的位置本身。 |
++cp |
表达式增加了指针变量cp 的值。表达式的结果是增值后的指针的一份拷贝,因为前缀++先增加它的操作数的值再返回这个结果。所以作为右值而言, 它是ch 的地址值加1的地址值。同样由于该位置未清晰定义,故而不能作为左值。 |
cp++ |
作为右值使用的话,它表示cp 本身的值,也就是ch 的地址值;如果cp++ 作为左值,会对cp 进行加1操作后在被赋值,由于cp++ 代表的地址未有清晰的定义,所以不能作为左值。 |
*++cp |
相当于*(++cp) ,作为右值,cp 作为指针变量的值加1后,也就是ch 的地址值加1,得到一个新的地址,加上间接访问符之后,便是取该地址对应的内存中的内容;作为左值,显然是cp +1 指向的那个地址本身,也即是ch 的地址加1后的那个新地址本身。 |
*cp++ |
相当于*(cp++) ,作为右值;是取cp 指向的地址(即ch 代表的地址&ch )的内容。这里也就是字符'a' ;作为左值,这个表达式就表示cp 指向的地址本身,也即是ch 的地址。 |
++*cp |
相当于++(*cp) ,作为右值的,*cp 的意思是去cp 指向的地址的值,这里为'a' ,然后执行++ 操作,也就是加1 ,那么表达式的值为字符'b' ;*cp 作为左值的意思是cp 指向的那个地址本身,也即是ch 代表的地址,再进行++ 操作,也就是地址加1 ,得到的新地址未清晰定义,所以不能作为左值。 |
(*cp)++ |
作为右值,就是先取cp 指向的地址的值,然后++ ,这样得到的就是'b' ,作为左值,显然是非法的,因为最后执行的是++ 操作。或者说,*cp 作为左值,代表cp 指向的地址本身,再进行++ ,得到的是一个未清晰定义的地址,不能作为左值。 |
++*++cp |
等价形式++(*(++cp)) ,由于最后进行的是++操作,所以不能作为左值;作为右值的情况,先进行++cp 操作,是cp 的地址加1 得到的一个新地址(&ch + 1) ,之后进行间接访问操作,取新地址对应的内存存储的的值,再次进行++ ,是对这个新地址对应的内存中的值加1 。 |
++*cp++ |
等价形式++(*(cp++)) ,同样,不能作为左值使用,因为最后操作的是++ ;作为右值的话,cp++ 中使用的是后缀++ ,故参与表达式运算的是cp 的一份拷贝,之后再进行cp 加1 操作,*(cp++) 取cp 指向的地址的内容,即'a' ,之后对'a' 加1得到'b' 。 |
指针的高级声明
指针的指针
int i;
int *pi = &i;
int **ppi = π
数组指针
数组指针是一个指针,它指向一个数组。
int (*p)[10]; // 声明一个数组指针 p ,该指针指向一个数组,这个数组包含10个整型数
函数指针
int (*f)(); // f是函数指针,指向的函数返回一个int类型
int (*f[])(); // f是一个数组,数组元素的类型是函数指针,它所指向的函数返回一个int类型
int *(*g[])(int,float) // g是一个数组,数组的元素是函数指针,它所指向的函数包含int、float参数,返回int*
声明一个函数指针并不意味着可以马上使用,和其它类型的指针一样,对函数指针执行间接访问之前必须把它初始化为指向某个函数。
例如:
int (*pf)(int) = &f;
&
操作符是可选的,因为函数名被使用时总是由编译器把它转换成函数指针。
&
只是显示的地说明了编译器隐式执行的任务。
函数调用的方式:
int ans;
ans = f(1);
ans = (*pf)(1); //把函数指针解引用为函数名,这个转换起始并非真正需要,编译器还是会将函数名转换成函数指针
ans = pf(1);
函数指针应用
回调函数
用户把一个函数指针作为参数传递给其它函数,后者将回调用户传递进来的函数,这种技巧称为回调函数。
当我们在在链表中查找一个数时,我们一般会这样写:
Node *search_list( Node *node, int const value )
{
while ( NULL != node ){
if ( node->value == value ){
break;
}
node = node->link;
}
return node;
}
这样就限制我们只能在查找的数必须是int类型,当变为其他类型时我们就无法用这个函数,但是重新写一个函数,重复代码又太多。
回调实现:
int compare_int( void const *a, void const *b )
{
if ( *( int * )a == *( int * )b ){
return 0;
}
return 1;
}
Node *search_list(Node *node, void const *value,
int (*compare)(void const *, void const *)) //函数指针
{
while(node != NULL){
if(compare(&node->value, value) == 0) //相等
break;
node = node->link;
}
return node;
}
这样利用回调函数就可以解决如上问题。我们把一个函数指针( int (*compare)(void const *, void const*) )
作为参数传递给查找函数,查找函数将“回调”比较函数。当我们需要执行不同类型的比较时我们合理调用该函数。
转移表
假设有程序:
switch (oper)
{
case ADD:
result = add(op1, op2);
break;
case SUB:
result = sub(op1, op2);
break;
case MUL:
result = mul(op1, op2);
break;
case DIV:
result = div(op1, op2);
break;
......
}
switch语句很长,所以,在这里用转移表来使问题得以简化。
声明并初始化一个函数指针数组:
double add(double, double);
double sub(double, double);
double mul(double, double);
double div(double, double);
......
// 声明一个函数指针数组
double (*oper_func[])(double, double) = {
add, sub, mul, div,......
};
// 函数调用
result = oper_func[oper](op1, op2);
这样就可以将具体操作和选择操作的代码分开。
命令行参数
int main(int argc,char *argv[])
int main(int argc,char **argv)
argc : 命令行传入参数的总个数
argv : *argv[]是一个指针数组,里面存放的指针指向所有的命令行参数,argv[0]指向程序的名称,argv[1]指向在执行程序名后的第一个字符串,argv[2]指向第二个。
字符串常量
字符串常量的本质类型是指向字符的指针。
"xyz"+1 // 结果是一个指针,指向'y'
*"xyz" // 结果是'x'
"xyz"[2] // 结果是'z'