Linux C开发基础知识复习下

宏定义

使用#define指令定义一个宏
使用#undef指令删除一个宏

之前说用#define来定义常量,实际上就是利用宏的预处理,进行字符串替换而已。现在我们就使用gcc命令要验证

编写以下代码main.c

#define PI 3.14

int main(){
    int r = PI *10 + PI*PI;
    return 0;
}

不生成文件了,直接在命令行打印预处理结果

gcc -E main.c

输出:

# 1 "main.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "main.c"


int main(){
    int r = 3.14 *10 + 3.14*3.14;
}

可以很清楚的看到,预处理之后,将所有的PI进行了文本替换。

 

条件编译

包含#if#ifdefifndef等,使预处理器可以根据条件确定是否将一段文本包含

条件编译就更简单了,修改main.c

define PI 3.14

int main(){
   int a = 0;
#if 0
    int r = PI *10 + PI*PI;
#endif
   return 0;
}

预编译输出

# 1 "main.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "main.c"


int main(){
    int a = 0;
}

可以看到,当使用条件预处理指令#if时,判断的条件为0,直接就将包裹的代码删除了,实际上在真正的编译之后的程序中 ,根本就不存在这些内容,等同你从来没写过。

关于预编译指令,需要记住几点

  1. #开头的预处理指令必须顶格写,前面不要有空格

  2. 记住三大类预处理指令的特点,#include指令是声明展开,宏定义是文本替换,条件编译是直接删除代码。

 

带参的宏

也称函数式宏,宏函数。

#define 标识符(a,b,c,...,d) 替换列表
#define MAX(x,y) ((x)>(y)?(x):(y))

如上,预处理器会在后面将所有的MAX(x,y)替换为后面替换列表的内容,其中x、y分别对应后面替换列表中的x、y

关于宏函数的注意事项

max = MAX(i++,j);

如上例,错误的使用宏函数,可能得到预期之外的结果,上例在预处理之后,被替换为如下代码,i会被加两次:

max = ((i++) > (j)?(i++):(j));

关于小括号的注意事项
1、如果宏替换列表中有运算符号,那么必须将整个替换列表放入小括号中
#define TOW_PI (2*3.14)

2、如果宏有参数,那么每个参数在替换列表中出现时,都要放在小括号中
#define MAX(x,y) ((x)>(y)?(x):(y))

运算符
宏定义包含两个专用运算符###

  • # 运算符可以用来字符串化宏函数里的参数,它出现在带参数宏的替换列表中。

 

关于宏的一些总结
 

  1. 使用宏函数,可以减少函数栈的调用,稍微提升一点性能,相当于C++中的内联的概念,在C99中也实现了内联函数的新特性。缺点是宏展开后,增加了编译后的体积大小。

  2. 宏参数没有类型检查,缺少安全机制。

  3. 宏的替换列表可以包含对其他宏的调用

  4. 宏定义的作用范围,直到出现这个宏的文件末尾

  5. 宏不能被定义两次,除非新定义与旧定义完全一样

  6. 可以使用#undef 标识符取消宏定义,若宏不存在,则该指令没有作用

 

  • defined运算符

    #define DEBUG
    
    #if defined DEBUG
    ...
    #endif
    

    检测其后的标识符是否有定义过,若定义过则返回1,否则返回0

  • #ifdef#ifndef
    #ifdef指令用于检测一个标识符是否已经被定义为宏,#ifndef则相反,检测一个标识符是否未被定义为宏

    #ifdef 标识符
    
    /* 它等价于以下指令 */
    #if defined 标识符
    
  • #elif#else
    这两个指令结合#if使用,相当于C语言中的if…else if…else的用法。这两个指令还可以与#ifdef#ifndef结合使用

    #if 表达式1
    ...
    #elif 表达式2
    ...
    #else
    ...
    #endif
    

条件编译主要可以用于
1、需要测试调试代码时,打印更多信息,正式发布时则去除这些代码
2、跨平台,跨编译器。对于不同平台,可以包含不同的代码,使用不同的编译器特性
3、屏蔽代码。使用注释符号注释代码时,有一个缺点,注释无法嵌套,即不能注释中间包含注释的代码,使用条件编译则很方便

 

static关键字

除了局部变量和全局变量,C语言中还有静态局部变量和静态全局变量,声明时使用static关键字修饰即代表静态的意思。

#include <stdio.h>

// 静态全局变量
static int s_global;

int get_count(){
    // 静态局部变量
    static int count;
    count++;
    return count;
}

int main(){
    printf("%d\n",get_count());
    printf("%d\n",get_count());
    printf("%d\n",get_count());
    printf("%d\n",get_count());
 
    return 0;
}

静态全局变量和普通全局变量的区别不是很大,主要体现在访问权限的区别上。在C语言中,全局变量是在整个程序的生命期中都有效的,换句话说,也就是一旦声明了一个全局变量,则整个程序中都可以访问,而静态全局变量,则只在声明它的那个源文件中可以访问。静态全局变量虽然也是在整个程序的生命期中都有效,但它在其他文件中不可见,无法被访问。关于这一点的细则,在下面的extern关键字的使用中做详细说明。

静态局部变量和普通局部变量的区别就比较大了,主要有三个区别

  1. 存储位置不同。静态局部变量被编译器放在全局存储区,虽是局部变量,但是在程序的整个生命期中都存在。而普通局部变量在函数调用结束后就会被释放。从这一点上看,静态局部变量和全局变量被放在了相同的储存位置。

  2. 静态局部变量会被编译器自动初始化为零值。我们都知道普通局部变量的原则是先初始化后使用,而静态局部变量则和全局变量一样,会被自动初始化,使用时只需声明,无需手动初始化。

  3. 静态局部变量只能被声明它的函数访问。静态局部变量与普通局部变量的访问权限相同,都只能被声明它的函数使用。如上例,静态局部变量count只能被get_count函数使用,即使count变量在整个程序的生命期中都有效,其他函数也无法使用它。

说完了静态局部变量后,大家肯定疑惑,既然它只在声明它的函数中使用,那它还有什么意义呢?直接使用普通局部变量不就行了,干嘛要用它?我们知道,普通局部变量在函数每次被调用的时候都会生成一个新的,调用结束后又将它释放,如果一个函数被频繁调用,这样性能岂不是很低?因为需要不停的生成新的局部变量,然后又释放掉,然后又生成新的……但是给局部变量加上了static修饰后,函数无论被调用多少次,都不会再生成新的局部变量,始终都是复用的同一个变量,这就大大减少了对内存的操作,提升了性能。

举个生活中的例子,如果你在公司楼下有一个固定的私人停车位,那么你每天上班只需要把车停在固定的地方就好,如果你没有私人停车位,那你每天到公司楼下,都需要四处去找一个空位子停车,岂不是很麻烦,效率又低,弄不好因为找停车位导致打卡迟到。

既然静态局部变量这么好,那是不是可以滥用呢?还是回到上面的例子,如果你是公司特聘人员,一个月只需要上两天班,那么你有必要在公司楼下买一个固定的私人停车位吗?显然是没有必要的,因此当函数不会被频繁调用时,不应当考虑使用静态局部变量。

最后需要特别注意,静态局部变量会一直保存上次的值,因为它一直都存在。基于这个特性,我们通常可以使用静态局部变量做计数器,如上例,每次调用get_count函数时,对静态局部变量count自增1,打印结果如下:

1
2
3
4

静态函数static关键字除了可以修饰变量,还可以用来修饰函数。在C++、Java等面向对象的编程语言中,都存在类似于private的权限访问控制,而C语言中的static关键字,就类似这种private,被它修饰的函数只能在当前源文件中使用,在其他源文件中无法被访问。通常来说,C语言编写的大型的模块化工程中,不需要共享的函数都应该使用static关键字来修饰。

需要特别注意,由于C语言没有命名空间的概念,它只有一个全局作用域,当你的C程序十分庞大时,存在几百上千个函数时,很难保证函数不会同名。当然,通过严格的代码规范,命名规范,可以人为的保证函数不会同名,但我们可以保证自己写的函数不会同名,却无法保证引入的外部库的函数不会和我们的函数同名。一旦函数同名了,就会形成命名冲突,这就是为什么我们看一些C语言编写的开源库时,变量名、函数命名非常的复杂,名字很长,多个单词大写或以下划线分隔,这样怪异的命名很大程度上就是为了避免命名冲突。基于此,我们编写非公开、非共享的函数时,都应当使用static修饰,以此来避免一部分命名冲突问题。static修饰的函数,只在当前源文件中可见,在另一个源文件中声明一个同名的函数,就不会产生命名冲突。

 

extern关键字

在说明extern关键字前,先来看一个示例 编写t1.c

// 全局变量
int s_global=12;

编写main.c

#include <stdio.h>

int main(){
    printf("s_global=%d\n",s_global);
    return 0;
}

编译:gcc t1.c main.c -o main这样会直接报错:error: 's_global' undeclared (first use in this function)

这好像和我们前面说的有些不符,全局变量是在整个程序的生命期都有效的,在全局可访问的,但是现在却报错了。大家要注意前面的措辞,全局变量在文件作用域内可见,即从变量被声明的下一行,一直到当前文件的末尾,它都可以被直接使用。这里的关键就是直接使用,在t1.c源文件中是可以直接使用的,但是main.c中就无法直接使用了。

当全局变量离开了它的文件作用域后,无法直接使用,这时候我们需要另一个关键字extern来帮助我们使用它。

修改main.c

#include <stdio.h>
// 写在函数外部,表示在当前文件中的任意地方都可以使用s_global
// extern int s_global;

int main(){
	// 写在函数内部,仅在函数中使用
	extern int s_global;
    printf("s_global=%d\n",s_global);
    return 0;
}

再次编译成功,运行结果

s_global=12

在这里,extern int s_global;并不是重新声明变量的意思,它表示的是引用全局变量s_global,一定要注意,如果不存在一个全局变量s_global,是无法编译的,也就是说,使用extern来引用全局变量时,全局变量一定要存在。

extern主要是用来修饰变量的,当然也可以用来修饰函数,通常C语言中的函数都使用头文件包含的方式引入声明,但我们也可以使用extern修饰。实际上C语言中的函数声明默认都是包含extern的,无需手动指定。

//以下两种是等价的,无需手动指定extern关键字
int get_count();
extern int get_count();

小拓展有时候我们可能会看到extern “C”这样的声明,请注意,这不是C语言的语法,也不属于C语言。有些C++程序员,经常把C语言和C++语言搞混,实际上这是两种不同的语言,C++也并不是很多人说的那样,完全是C语言的超集,更准确的说法应该是,C++是一种独立的语言,它兼容C语言的绝大多数语法,但并不是百分百完全兼容。C++除了兼容的C语言的语法,另一部分就是它独立的内容。如果不能完全清楚这两种语言的边界,就会发生语法弄混的情况。

在C++中,当需要调用纯C语言编写的函数时,通常会使用extern “C”声明,表明这是纯C语言的内容。

 

头文件的嵌套包含

所谓嵌套包含,就是指在一个头文件中,还可以使用#include预编译指令,包含其他的头文件。例如,我们编写一个头文件bool.h

#define Bool int
#define False 0
#define True 1

在以上头文件中,我们使用宏定义了新类型Bool,接着编写func.h头文件

#include "bool.h"

// 声明一个函数,返回值为Bool类型,值可以是False 或者True
Bool check();

头文件的保护

如果一个源文件将同一个头文件包含两次,那么就可能会产生编译错误。因此,在C语言的模块化开发中,一定要避免将同一个头文件包含两次。但是,有时候这种包含不是明显的,而是一种隐式的包含,不易察觉,不知不觉就犯下了错误。

如下例,分别创建h1.hh2.hh3.h三个头文件h1.h内容

#include "h3.h"
……

h2.h内容

#include "h3.h"
……

可以看到,h1.hh2.h两个头文件分别都包含了一个相同的h3.h头文件,那么如果在main.c中分别包含这两个头文件

// main.c
#include "h1.h"
#include "h2.h"
……

这样一来,实际上就等同于在main.c中将h3.h头文件include了两次,显然违背了我们上面说的,不能在一个源文件中将同一个头文件包含两次的原则。因为所谓头文件包含,实际上就是将头文件中的声明复制到当前源文件中,那么上例中h3.h一定会被复制两次。

问题出来了,该如何解决呢?在复杂的大型工程中,头文件被重复包含的问题一定是避免不了的,这个时候就需要我们上一章讲的条件编译知识出来救场了。

修改h3.h文件

内容如下

// 如果没有定义过_H_H3_ 宏,则定义一个_H_H3_ 宏
#ifndef _H_H3_
#define _H_H3_

// 声明的内容 ……

#endif

改造头文件之后,再去源文件使用,就不会存在重复包含的问题了。

注意,这里使用#ifndef#endif将整个头文件中的全部内容包裹起来,然后在#ifndef之后通过#define定义一个宏,这样一来,#ifndef#endif之间的内容就只会被预编译一次,而不会重复包含。这种机制,被戏称为头文件卫士,或者称为头文件保护。如果对于这种写法不太理解,可以使用上一章介绍的gcc -E命令,生成预编译代码查看,即可明了。

最后,需特别注意的地方是宏的名字,这里是_H_H3_,使用头文件包含这种机制时,宏定义的名字一定要独特,避免重复,以免导致各种不可预知的问题。通常宏的名字要全部大写,并用下划线来分隔单词或缩写,在这个宏的名称中,最好包含当前头文件的文件名,例如H3

在C语言中,我们以后自己编写头文件,建议在所有编写的头文件中都使用这种头文件保护机制,因为你不知道什么时候,你的这个头文件可能就会被重复包含,如上例,h1.hh2.hh3.h三个头文件都应当使用头文件保护机制。

 

C语言内存管理内容

C语言程序加载到内存中,通常可人为划分为栈(stack)、堆(heap)、代码段(text)、数据段(data)、bss 段、常量存储区等区域部分,在这个基础上,人们习惯在逻辑上将C语言程序的内存模型归纳为四大区域。请注意,这四大区域只是逻辑上的划分,实际上对于内存而言,它只是一片连续的存储单元,并不存在什么物理上的区域划分。我们了解C语言内存四区,可以加深对C语言的理解,特别是C语言的内存管理的理解。

 

内存四区

  • 栈(stack)用于保存函数中的形参、返回地址、局部变量以及函数运行状态等数据。栈区的数据由编译器自动分配、自动释放,无需程序员去管理和操心。当我们调用一个函数时,被称为函数入栈,指的就是为这个函数在栈区中分配内存。

  • 堆(heap)堆内存由程序员手动分配、手动释放,如果不释放,只有当程序运行结束后,操作系统才会去回收这片内存。C语言所谓的动态内存管理,指的就是堆内存管理,这也是C语言内存管理的核心内容。

  • 静态全局区又被人称为数据区、静态区。它又可细分为静态区和常量区。主要用来存放全局变量静态变量以及常量。该区域内存,只有在程序运行结束后才会被操作系统回收。被形象的比喻为与整个程序同生共死,也就是说只要程序没有退出,这部分内存数据就一直存在。

  • 代码区用于存放程序编译链接后生成的二进制机器码指令。由操作系统管理,程序员无需关心。

 

内存分配

C语言内存分配的三种形式

  1. 静态/全局内存 静态声明的变量和全局变量都使用这部分内存。在程序开始运行时分配,终止时消失。区别:所有函数都能访问全局变量,静态变量作用域则只局限于定义它的函数内部

  2. 自动内存 在函数内声明,函数调用时创建(分配在栈中),作用域局限于该函数内部,函数执行完则释放。

  3. 动态内存 内存分配在堆上,用完需手动释放,使用指针来引用分配的内存,作用域局限于引用内存的指针

为什么需要在堆上面分配动态内存?在前面的章节中,我们一直使用自动内存,也就是栈内存,这并不影响C程序的编写,那么我们为什么还要去使用动态内存,而且还要很麻烦的去手动管理动态内存呢?

  • 栈区的内存大小通常都比较小,具体大小视编译器不同而有所区别,通常可能会在2M大小左右。当我们在处理大文件、图片、视频等数据时,2M显然是不够用的,我们可能需要更大块的内存空间。通常的,堆内存空间大小是没有限制的,只要你电脑的内存条足够大,你就可以向操作系统申请足够大的堆内存空间使用。

  • 栈内存的使用有一定特殊性。通常当函数调用结束后就会退栈,那么函数中的局部变量也就不复存在了。当我们需要一个变量或数组有更长的生命周期时,堆内存是更好的选择。

  • 全局变量虽然有与程序相同的生命周期,但无法动态的确定大小。例如将数组声明为全局数组变量,那么就必须在声明时静态指定数组的长度。假如我们用一个数组来存放会员注册信息,那么我们根本不能在编译时确定数组的具体长度,显然的,我们需要一个可以动态增长的内存区域。不断有新会员注册,那么我们的数组长度也需要增长。

 

动态内存管理

在C语言内存分配的三种形式中,真正能由程序员来控制管理的只有在堆上面分配的动态内存,这也是我们需要关注的重点内容。

先看一个示例:

// 定义一个函数,返回局部变量的地址
int *fn(){
    int i = 10;
    return &i;
}

int main(){
	// 对fn函数返回的指针进行解引用
    printf("%d",*fn());
    return 0;
}

以上示例会报错退出,显然的,我们是不能返回一个局部变量的地址的,局部变量在函数调用结束会就会释放,因此在局部变量作用域之外去操作它的地址是非法操作。

使用动态内存

#include <stdio.h>
#include <stdlib.h>

int *fn(){
    // 使用malloc函数,分配动态内存空间,注意包含stdlib.h
    int *p = (int*)malloc(sizeof(int));
    *p = 16;
    return p;
}

int main(){
    printf("%d",*fn());
    return 0;
}

运行正常,打印结果:

16

可以看到,申请动态内存之后,如果不手动释放,它就会一直存在,直到程序退出。

可以看到malloc的函数原型 void *malloc(size_t _Size); 它返回一个void *类型指针,这是一个无类型或者说是通用类型指针,它可以指向任意类型,因此我们在使用它的返回值时,首先做了强制类型转换。该函数只有一个无符号整数参数,用来传入我们想要申请的内存大小,单位是字节。上例中我们传入的是一个int类型的大小,通常是4字节。需要特别注意,当使用malloc分配动态内存时,如果失败,它会返回NULL指针,因此使用时需判断。

在使用动态内存时,一定要在用完后记得手动释放内存,否则易造成内存泄露

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char *String(int len){
    char *s = (char*)malloc(len);
    return s;
}

int main(){
    char *str = String(100);
    if (str == NULL){
        // 内存分配失败时,返回NULL指针,使用时需先判断分配是否成功
        printf("Not enough memory space!\n");
    }
    
    strncpy(str,"Hi,use dynamic memory space",100);
    printf("%s\n",str);

    // 手动释放内存
    free(str);
    return 0;
}

二级指针

所谓二级指针,就是一个指向指针的指针。

我们知道指针变量是用来保存一个普通变量的地址的,那么如果对一个指针变量取地址,并用另一个变量保存指针变量的地址,这种情况是否存在呢?

int main(){
    int num = 16;
    // 声明一个一级指针
    int *p = &num;

    // 声明一个二级指针,一个指向指针的指针
    int **pp = &p;

    printf("p=%x\n",p);
    printf("pp=%x\n",pp);
    printf("&pp=%x\n",&pp);
    return 0;
}

打印结果:

p=22fe4c
pp=22fe40
&pp=22fe38

可以看到,凡是变量都有地址,即使是指针变量也是有地址的,这种使用两个*来声明的指向指针的变量,就是二级指针。一级指针存的是普通变量的内存地址,二级指针则是存的一个一级指针的内存地址。

在遇到二级指针时,要获取原始变量的值,就需要使用两个*进行解引用,如上例中的**p可获取num的值,如使用一个*解引用,获得的只是指针p的地址而已。

除了二级指针,自然还可以有三级指针、四级指针等等,然而后面的指针已经没有了意义。除了二级指针有较强的实际意义,其他的基本可以忽略。

引出了二级指针,有人一定会问,二级指针到底有什么用处,在哪里使用?下面看一个示例

int main(){
    // 使用二维数组保存英文歌单
    char songs[10][50]={
        "My love",
        "Just one last dance",
        "As long as you love me",
        "Because of you",
        "God is a girl",
        "Hero",
        "Yesterday once more",
        "Lonely",
        "All rise",
        "One love"
    };

    for (size_t i = 0; i < 10; i++){
        printf("%s\n",songs[i]);
    }
    return 0;
}

在C语言中,字符串是用字符数组来表示的,那么字符串数组也必然是一个二维数组,如上。在字符串的章节中讲过,C语言字符串也可以使用char*来表示,那么字符串数组也就可以使用二级指针char **来表示了。

void printStrings(char **s,int len){
    char **start = s;
    // 使用二级指针来遍历字符串数组
    for (;s < start + len;s++){
        printf("%s\n",*s);
    }
}

int main(){
    // 声明一个char*类型的数组,它的元素是一个char*指针
    char* songs[10]={
        "My love",
        "Just one last dance",
        "As long as you love me",
        "Because of you",
        "God is a girl",
        "Hero",
        "Yesterday once more",
        "Lonely",
        "All rise",
        "One love"
    };

	// 一个指针数组的数组名,实际上就是一个二级指针
    char **p = songs;
    printStrings(p,10);
    return 0;
}

二级指针的灵活运用

#include <stdio.h>
#include <string.h>

// 打印字符串数组
void printStr(char **s,int len){
    char **start = s;
    for (;s < start + len;s++){
        printf("%s\n",*s);
    }
}

void handleStr(char **buf,int size){
    char **tmp = buf;
    while (buf < tmp + size){
        //遍历字符串数组,如果包含"love",则修改为 520
        if(strstr(*buf,"love") != NULL){
            *buf = "520";
        };
        buf++;
    }
}

int main(){
    // 声明一个字符串数组
    char* songs[5]={
        "My love",
        "As long as you love me",
        "Because of you",
        "God is a girl",
        "Hero",
    };

    char **p = songs;
    handleStr(p,5);
    printStr(p,5);
    return 0;
}

函数指针

在上面的内存四区中提到了代码区,而函数就是一系列指令的集合,因此它也是存放在代码区。既然存放在内存中,那么就会有地址。我们知道数组变量实际上也是一个指针,指向数组的起始地址,结构体指针也是指向第一个成员变量的起始地址,而函数指针亦是指向函数的起始地址。

所谓函数指针,就是一个保存了函数的起始地址的指针变量

函数指针的声明

声明格式:

【返回值类型】 (*变量名) (【参数类型】)

实例

// 分别声明四个函数指针变量 f1、f2、f3、f4
int (*f1)(double);
void (*f2)(char*);
double* (*f3)(int,int);
int (*f4)();

函数指针的赋值与使用

当一个函数的原型与所声明的函数指针类型匹配,那么就可以将一个函数名赋值给函数指针变量。

int count(double val){
    printf("count run\n");
    return 0;
}

void printStr(char *str){
    printf("printStr run\n");
}

double *add(int a,int b){
    printf("add run\n");
    return NULL;
}

int get(){
    printf("get run\n");
    return 0;
}

int main(){
    // 声明函数指针并初始化为NULL
    int (*f1)(double) = NULL;
    void (*f2)(char*) = NULL;
    double* (*f3)(int,int) = NULL;
    int (*f4)() = NULL;

    // 为函数指针赋值
    f1 = &count;
    f2 = &printStr;
    f3 = &add;
    f4 = &get;

    // 使用函数指针调用函数
    f1(0.5);
    f2("f2");
    f3(1, 3);
    f4();
    return 0;
}

函数名同数组名一样,它本身就是一个指针,因此可以省略取地址的操作,直接将函数名赋值给指针

    f1 = count;
    f2 = printStr;
    f3 = add;
    f4 = get;

函数指针的传递

// 加法函数
int add(int a,int b){
    return a +b;
}
// 减法函数
int sub(int a, int b){
    return a-b;
}

// 计算器函数。将函数指针做形式参数
void calculate(int a,int b, int(*proc)(int,int)){
    printf("result=%d\n",proc(a,b));
}

int main(){
    // 算加法,传入加法函数
    calculate(10,5,add);
    // 算减法,传入减法函数
    calculate(10,5,sub);
    return 0;
}

可以看到,将函数指针作为参数传递,可以使得C语言编程变得更加灵活强大。而在Python、JavaScript等编程语言中,当前流行的函数式编程范式,即将一个函数作为参数传入到另一函数中执行,实际上有些古老的C语言中早就能实现了。

除此之外,C语言还有其他的一些奇技淫巧,虽然看起来实现得不够优雅,但也足以证明C语言无所不能。

以上在函数的形参中直接定义函数指针看起来不够简洁优雅,每次都得写一大串,实际上还有更简洁的方式,这就需要借助typedef

// 定义一个函数指针类型,无需起新的别名
typedef int(*proc)(int,int);

// 使用函数指针类型 proc 声明新的函数指针变量 p
void calculate(int a,int b, proc p){
    printf("result=%d\n",p(a,b));
}

函数指针实用小结

  1. 利用函数指针可以实现函数式编程

  2. 将函数指针存入数组中,可以像Java、Python这样,实现函数回调通知机制

  3. 将结构体与函数指针结合,可以模拟面向对象编程中的类。实际上Go语言就是这样做的,Go语言没用类机制,就是使用结构体模拟面向对象编程。

 

void*指针

前面几次提到通用类型指针void*,它可以指向任意类型,但对于void*指针到底是什么没有做深入的探讨。事实上,只有理解了void*指针,才能真正理解C语言指针的本质,才能使用void*指针实现一些奇技淫巧。

首先思考一个问题,指针仅仅是用来保存一个内存地址的,所有的内存地址都只是一个号码,那么指针为什么还需要类型呢?理论上所有的指针都应该是同一种类型才对呀?

先写个代码探索一番

int main(){
    short num = 18;
    
    char *pChar = (char*)&num;
    int *pInt = (int*)&num;

    printf("pChar=%x pInt=%x\n",pChar,pInt);
    printf("*pChar=%d *pInt=%d\n",*pChar,*pInt);
    return 0;
}

分别强制使用char*指针和int*指针来保存short num的地址,打印结果如下:

pChar=22fe3e ---- pInt=22fe3e
*pChar=18 ---- *pInt=-29491182

可以看到,保存地址是OK的,但是解引用获取值就会存在问题。由此我们基本可以推断一个事实,指针用来保存变量的内存地址与变量的类型无关,任何类型指针都可以保存任何一个地址;指针之所以需要类型,只与该指针的解引用有关

short是2个字节,char是1个字节,int是4个字节,而指针保存的是第一个字节的地址,当指针声明为short时,编译器就知道从当前这个地址往后取几个字节作为一个整体。如果指针没有具体类型,那么编译器根本无法判断应从当前这个字节往后取几个字节。如上例,*pInt解引用后结果错误,这就是因为原类型是short2字节,而使用int*指针去解引用会超出short本身的两字节内存,将紧随其后的两字节内存也强制读取了,访问了不合法的内存空间,这实际上是内存越界造成的错误值。

当我们不确定指针所指向的具体数据类型时,就可以使用void*类型来声明,当我们后续确定了具体类型之后,就可以使用强制类型转换来将void*类型转换为我们需要的具体类型。

接触过Java等具有泛型的面向对象编程语言的人,可能马上就会联想到泛型,是的,C语言没有泛型,但是利用void*指针的特点,我们可以使用一些技巧来模拟泛型编程。

再看一个示例

// 交换两个变量的值
void swap(int *a,int *b){
    int tmp =*a;
    *a = *b;
    *b = tmp;
}

int main(){
    int n = 6, l=8;
    swap(&n, &l);
    printf("n=%d  l=%d\n",n,l);
    return 0;
}

 

以上swap函数是交换两个int类型变量的值,如果如果需要交换charshortdouble类型呢?岂不是每一种类型都需要写一个函数吗?像Java这样的编程语言存在泛型,我们可以定义泛型,而不需要在函数声明时指定具体类型,当调用的时候传入的是什么类型,函数就计算什么类型,我们看一下C语言如何实现

// 交换两个变量的值
void swap(void* a, void *b, int size){
    // 申请一块指定大小的内存空间做临时中转
    void *p = (void*)malloc(size);

    // 内存拷贝函数,拷贝指定的字节数
    memcpy(p, a, size);
    memcpy(a, b, size);
    memcpy(b, p, size);

    // 释放申请的内存空间
    free(p);
}

int main(){
    int n = 6, l=8;
    // 传入int型指针
    swap(&n, &l,sizeof(int));
    printf("n=%d  l=%d\n",n,l);

	// 传入short型指针
    short x=10,y=80;
    swap(&x, &y,sizeof(short));
    printf("x=%d  y=%d\n",x,y);
    return 0;
}

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值