指针

内存和地址

内存其实就是一组有序字节组成的数组,数组中,每个字节大小固定,都是 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的一份拷贝,之后再进行cp1操作,*(cp++)cp指向的地址的内容,即'a',之后对'a'加1得到'b'

指针的高级声明

指针的指针

int i;
int *pi = &i;
int **ppi = &pi;

数组指针

数组指针是一个指针,它指向一个数组。

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'

posted @ 2019-09-15 10:18  youngliu91  阅读(318)  评论(0编辑  收藏  举报