欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【C语言总结】C语言高级指针

程序员文章站 2022-06-09 13:03:00
...

1.指向指针的指针

这里有一个例子:

int i;
int* pi;
int** ppi;

1.printf( "%d\n", ppi );
2.printf( "%d\n", &ppi );
3.*ppi = 5;

【C语言总结】C语言高级指针

1.如果ppi是个自动变量,它就未被初始化,这条语句将回打印一个随机值,如果它是个静态变量,这条语句将会打印0

2.这条语句把ppi的地址作为十进制整数打印出来

3.因为ppi未被初始化,所以不应该对它进行间接访问操作,所以这条语句的结果不可预测

接下来这两条语句的用处比较大:

ppi = π
*ppi = &i;

第一个语句把ppi初始化为指向指针变量pi

第二个语句把pi(通过ppi间接访问)初始化为指向变量i

所以下面的语句具有相同的效果:

i = 'a';
*pi = 'a';
**pi = 'a';

2.高级声明

请看下面的声明:

int f;     /*一个整型变量*/
int* f;    /*一个指向整型的指针*/

第二个表达式,把表达式*f声明为一个整数,所以f是个指向整型的指针。所以,用于声明变量的表达式和普通的表达式在求值时所使用的规则相同。

看下面的声明:

int (*f) ();

第一对花括号只起到聚组的作用。它迫使间接访问在函数调用之前进行,使f称为一个函数指针,它所指向的函数返回一个整型值。

程序中每个函数都位于内存中的某个位置,所以存在指向那个位置的指针是完全可能的。

请看下面的声明:

int f[];
int *f[];
int f()[];
int f[]();
int (*f[])();
int *(*f[])();
int (*f)(int, float);
int *(*g[])(int, float);

第一个声明是声明一个整型数组

第二个声明,还是声明了一个数组,但是它的元素是指向整型的指针。

第三个声明是非法的,f是一个函数,它的返回值是一个整型数组——但是函数只能返回标量值,不能返回数组。

第四个声明也是非法的,看起来好像f是一个数组,返回值是为整型的函数,但是数组元素必须具有相同的长度。

第五个声明是合法的,f是一个数组,数组的元素是类型是函数指针,所以这个声明是一个函数指针数组。它所指向的函数的返回值是一个整型值。

第六个声明和第五个声明的唯一区别是多了一个间接访问操作符,所以这个声明创建了一个指针数组,数组的元素类型也是函数指针,只不过指针所指向的类型是返回值为整型指针的函数

第七个声明把f声明为一个函数指针,它所指的函数接受两个参数,分别是一个整型值和浮点型值,并返回第一个整型值。

第八个声明把g声明为一个数组数组的元素类型是函数指针,指针指向的函数接受两个参数,分别是一个整型值和一个浮点值,这个函数的返回值是一个整型指针

3.函数指针

函数指针最常用的两个用途是转换表作为参数传递给另一个函数

简单声明一个函数指针并不意味着它马上就可以使用,对函数指针执行间接访问之前必须把它初始化为指向某个函数。

int f(int);
int (*pf)(int) = &f;

第二个声明创建了函数指针pf,并把它初始化为指向函数f,函数指针的初始化也可以通过一条赋值语句来完成。

初始化表达式中的&操作符是可选的,因为函数名被使用时总是由编译器把它转换为函数指针。&操作符只是显式的说明了编译器将隐式的执行任务。

我们可以使用三种方式调用函数:

int ans;
ans = f(25);
ans = (*pf)(25);
ans = pf(25);

第一条语句简单的使用名字调用函数f,函数名f首先被转换为一个函数指针,该指针指定函数在内存中的位置,然后函数调用操作符调用该函数。

第二条语句对pf执行间接访问操作,它把函数指针转换为一个函数名。这条语句的效果和第一条语句一样

第三条语句和前面两条语句一样,间接访问操作并非是必须的

3.1回调函数

这里有一个简单的函数,用于在单链表中查找一个值。

Node* LinkList_Search(Node* node, const int value) {
        while(node != NULL) {
            if(node->value == value) {
                break;
            }   
            node = node->next;
        }
        return node;
}

这个函数看上去很简答,但是它只用于值为整数的链表,如果需要在字符串链表中查找就不得不另外编写一个函数。

有种更为通用的方法是使查找函数与类型无关。方法是使用函数指针,调用者编写一个函数用于比较两个值,然后把一个指向这个函数的指针作为参数传递给查找函数。然后查找函数调用这个函数来进行值的比较

第二个方面是向函数传递一个指向值的指针而不是值本身,函数有一个void*形参用于接受这个参数。然后这个值的指针便传递给比较函数。这个修改使字符串和数组对象也可以被使用。字符串和数组无法作为参数传递给函数,但是它们的指针却可以。

使用这种技巧的函数被称为回调函数。用户把一个函数指针作为参数传递给其他函数,后者将回调用户的函数。

与类型无关的链表查找:

#include<stdio.h>
#include"node.h"

/*比较函数的参数必须声明为void*以匹配查找函数的原型,然后它们再强制类型转换为int*值,用于比较整型值*/
int compare(const void* a, const void* b) {
    if(*(int*)a == *(int*)b) {
        return 0;
    }
    else {
        return 1;
    }
}
/*无关类型的链表查找*/
Node* search(Node* node, const void* value,
            int (*compare)(const void* ,const void*)) {

    while(node != NULL) {
        if(compare(&node->value, value) == 0) {
            break;
        }
        node = node->next;
    }
    return node;
}

3.2转移表

下面的代码取自一个程序,用于实现一个袖珍式计算器。下面代码对操作符进行测试然后决定要调用哪个函数:

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_fun[])(double, double) = {
    add,sub, mul, div, ..
};

初始化列表中各个函数名的正确顺序取决于程序中用于表示每个操作符的整型代码。

第二个步骤是用下面这条语句替换前面整条switch语句。

/*oper从数组中选择正确的函数指针*/
result = oper_fun[oper](op1, op2);

在转移表中。越界下标引用像普通数组一样是不合法的。

4.命令行参数

4.1传递命令行参数

C程序的main函数具有两个形参,第一个通常称为argc,他表示命令行参数的数目第二个通常称为argv,它指向一组参数值。由于参数的数目并没有内在的限制,所以argv指向这组参数值的第一个元素。这些元素的每个都是指向一个参数文本的指针,如果程序需要访问命令行参数,main函数在声明时就要加上这些参数:

int main(int agrc, char** argv)

1. argv是一个指针数组,要明白它的存数格式。它的每个元素都是一个字符指针,数组的末尾是一个NULL指针

  1. 也是由于argv是一个指针数组,它指向数组的第一个元素(是一个字符指针),所以argv是一个指向字符指针的指针,所以可以将其声明为char **argv。即声明为:【 int main( int argc, char **argv ) 】

  2. 参数名称是不重要的,你可以声明为其它的名字,比如 【 int main( int a, char *b[] ) 】 都是可以的。

  3. 指针数组argv的第 1 个元素,即argv[0],是程序名,也就是你的可执行文件的名字

  4. 如果你输入的参数中有空格,应该使用双引号括起来。

$ cc -c -o main.c insert.c -o test

如下图:

【C语言总结】C语言高级指针

这个数组的每个元素都是一个字符指针,数组的末尾是一个NULL指针。argc的值和这个NULL值用于确定实际传递了多少个参数。argv指向数组的第一个元素,这就是为什么它被声明为一个指向字符的指针的原因。

下面的代码是打印命令行参数的代码:

#include<stdio.h>

int main(int argc, char** argv) {
    while(*++argv != NULL) {
        printf("%s\n", *argv);
    }
    return EXIT_SUCCESS;
}

4.2处理命令行参数

文件名参数前面的选项参数,在程序名后面, 可能有零个或多个选项,后面跟零个或多个文件名,像下面这样:

prog -a -b -c name1 name2 name3

每个选项都以一条横杠开头,后面是一个字母,用于在几个可能的选项中表明程序所需的一个。下面的代码检查所有的参数。

#include<stdio.h>
#define TRUE 1

/*执行是级任务的函数的原型*/
void process_standard_input(void);
void process_file(char* file_name);

//选项标志,缺省初始化为FALSE
int option_a, option_b;

void main(int argc, char** argv) {
    while(*++argc != NULL && **argv != '-') {
        switch(*++*argv) {
        case 'a':
            option_a = TRUE;
            break;
        case 'b':
            option_b = TRUE;
            break;
        }
    }
    //处理文件名参数
    if(*argv == NULL) {
        process_standard_input();
    }
    else {
        do {
            process_file(*argv);
        }while(*++argv !+ NULL);
    }
}

5.字符串常量

字符串是个常量,编译器把这些指定字符的一份拷贝存储在内存中的某个位置,并存储指向第一个字符的指针。但是当数组名用于表达式时,它们的值也时指针常量。我们可以对它进行下标引用、间接访问已经指针运算。

"xyz" + 1;

字符串常量是个指针,所以这个表达式的结果是指针,指向字符串的第二个字符‘y’。

*"xyz";

可知这个表达式是这个字符串的第一个字符:x。

"xyz"[2];

这个表达式是字符z。

*("xuz" + 4);

这个表达式是个不可预测的字符,因为越界了。

一些声明:

//返回值为int类型的函数
int abc();
//int型数组
int abc[3];
//返回值为“int型指针的指针”的函数
int **abc();
//返回值为int的函数指针
int (*abc)();
//指向“int型数组”的指针
int (*abc)[6];
//返回值为“int型指针”的函数
int *abc();
//指向“返回值为int型指针的指针的函数”的指针的数组
int **(*abc[6])();
//指向“int型指针数组”的指针
int **acc[6];
//指向“int型指针数组”的指针
int *(*abc)[6];
//返回值为“返回值为int型指针的函数指针”的函数
int *(*abc())();
//返回值为“返回值为int的函数指针的指针”的函数指针
int (**(*abc())();
//返回值为“指向int型数组的指针”的函数指针
int (*(*abc)())[6];
//返回值为“指向‘返回值为int型指针的函数指针’的数组的指针”的函数指针
int *(*(*(*abc)())[6])();