C与C++之指针(五)

阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680


1. 为什么要引入指针

软件开发/设计行业有这么一句话:没有什么是不能通过增加一个抽象层解决的。

这句话很对……但抽象层并不是免费的。一旦和某个实体之间有了抽象层:

1. 你必须间接访问该实体(如果实现的很好,有时候的确能够无需付出性能代价;但并不能保证任何时候都无代价)

2. 你必须以抽象者所期望的方式访问该实体:即便你知道该实体其实是什么、在处理某些问题时用不着七拐八绕,你也得七拐八绕着访问它。即:封装有时候反而会增加复杂度。

越是底层,抽象就越难做。因为,其一,稀奇古怪的需求实在太多,总有你想不到的地方;其二,如果你抽象了,那么就必须保证任何情况下,这个抽象都得真的像它所定义的那样工作;而这个往往意味着很多方面的代价。

比如说,数组就是对“一系列连续内存单元”的抽象;它对外表现为“一个固定大小的容器”。

现在问题来了:如果有人访问第(数组大小+1)个元素,那么你就必须阻止他。于是,每次有人访问数组,你都得先检查待访问元素的下标是否越界——这就导致每次访问,你都必须付出几倍的时间代价。

而对C来说,数组就是一个指向一片内存区域的指针……它并不去封装这个概念;恰恰相反,它鼓励你去了解藏在表象背后的东西。

于是乎,举例来说,在大量文本中搜索匹配某个模式的字符串(即strstr函数),如果C用3秒能搜完,其它语言再快可能也得9秒。因为每和一个字符比较,其它语言都要多两次索引越界与否的检查动作。

当然,这个好处并不是白捡到的。C语言用户因此而付出的代价,就是防不胜防的缓冲区溢出问题……

再看一个例子。假设我们实现底层网络包的识别/分析工作(就好像wireshark那样),我们需要:

1、分析包的来源、去向、类型、控制数据等信息

2、分析TCP/UDP包的内部信息可能是哪个已知协议(http、https、msn、ssh等等等等)、并输出分析结果(如果无法识别,以16进制数字显示)

3、可以很容易的扩充支持的协议(比如加入QQ、WOW之类协议的支持)

如果使用C和指针:

1、按照IP报头规范,读取正确偏移位置的几个字节,识别出包的来源地址和目标地址

2、根据报头某个位置的标记,识别这是一个TCP包还是一个UDP包

3、以便于阅读的方式,输出TCP/UDP/IP头携带的信息

4、拆分出载荷,把载荷首地址、长度等信息丢给一边蹲着的协议分析器链

5、第一个协议分析器按照已知的网络封包协议定义,识别载荷是不是自己能对付的那种协议

6、如果是,分析之,并输出分析结果;否则,丢给下一个协议分析器处理

7、如果所有协议分析器都无法识别,则该包可能是私有格式,按默认格式显示

整个流程甚至可以直接在指针指向的那片内存上进行,无需任何复制动作——直接就是真正的0 copy。

在协议分析器内部,你只需:检查长度是否足够;把传来的指针强制类型转换成自己支持的数据结构(如 struct msnHead之类);检查数据结构中各项的值是否正确;如果正确,按标准格式输出到分析结果。

有的时候,数据就是数据、函数就是函数。封装成类,反而棘手多了。

尤其是这类偏底层、偏数据和算法方面的应用,和高层的UI开发不同,类经常是个累赘。

指针并不仅仅把数据当作数据,内存当作内存;它甚至允许你把硬件看成硬件,你完全可以取局部变量的地址、然后顺藤摸瓜,把整个栈空间打印出来。

所以,C的好处就是:没有多余的抽象/封装;一切以硬件界面为准,什么东西是什么,它就是什么。你可以在其上无限的发挥想象力——哪怕搞个自己的类体系也不是是小菜一碟——没有任何限制,没有任何思维负担。

所有这些,都是围绕着指针实现的。

当然,这样也不是没有代价的:对java来说,一个对象是什么,它就是什么;一个类说我保证什么、你不能碰什么,你就只能照做。这是语言提供的保证,所以你很难做错事。这就是封装的好处。

但对C来说,别人给你一堆数据,这些数据也很可能是通过某种方式“惫懒”来的,你最好不要随意动它;操作系统源码里,看起来平平常常几行代码,很可能访问的是了不得的区域;有时候,除非阅读源码、遵循各种编码规范并且祈祷别人也遵循它们,你得不到任何保证——得到“无比犀利、无比直接的解决某些问题”的能力同时,你可能也得到了无比犀利、无比奇葩的BUG……

要用好C,你必须能够看透数据的本质、必须能看透别人代码的意图(并不会有编译器帮助你、告诉你什么不能碰)、必须知道自己写下的每一行代码意味着什么并自己为它所可能造成的任何side effect负责(所以对新手来说,单步执行并观察每行代码造成的所有影响,是入门所必不可少的一步):如果做不到,你就会变成团队里的麻烦制造者——在这些要求面前,精通指针只能算刚刚入门罢了。

2. 常用的指针类型

int p; //这是一个普通的整型变量 

int *p; //首先从P 处开始,先与*结合,所以说明P 是一个指针,然后再与int 结合,说明指针所指向的内容的类型为int 型.所以P是一个返回整型数据的指针 

int p[3]; //首先从P 处开始,先与[]结合,说明P 是一个数组,然后与int 结合,说明数组里的元素是整型的,所以P 是一个由整型数据组成的数组 

int *p[3]; //首先从P 处开始,先与[]结合,因为其优先级比*高,所以P 是一个数组,然后再与*结合,说明数组里的元素是指针类型,然后再与int 结合,说明指针所指向的内容的类型是整型的,所以P 是一个由返回整型数据的指针所组成的数组 

使用:

int* p[2];
int a[3] = {1, 2, 3};
int b[4] = {4, 5, 6, 7};
p[0] = a;
p[1] = b;
for(int i = 0; i < 3; i++) cout << *p[0] + i;// cout << **p + i;
cout << endl;
for(i = 0; i < 4; i++) cout << *p[1] + i;// cout << **p + i;

int (*p)[3]; //首先从P 处开始,先与*结合,说明P 是一个指针然后再与[]结合(与"()"这步可以忽略,只是为了改变优先级),说明指针所指向的内容是一个数组,然后再与int 结合,说明数组里的元素是整型的.所以P 是一个指向由整型数据组成的数组的指针 

相当于一个二维数组的用法,只是它是一个n行3列的数组,可以这样来用:

#include 
int (*p)[2];
int b[3][2] = {{1, 2}, {3, 4}, {5, 6}};
p = b;
for(int i = 0; i < 3; i++) {
  for(int j = 0; j < 2; j++) //cout << p[i][j]; //cout << *(*(p+i)+j);
  cout << endl;
}

对于(1)为行数确定、列数不确定,即为2*n型的。(2)为n*2型的数组的指针用法,即行数不确定、列数确定

int **p; //首先从P 开始,先与*结合,说是P 是一个指针,然后再与*结合,说明指针所指向的元素是指针,然后再与int 结合,说明该指针所指向的元素是整型数据.由于二级指针以及更高级的指针极少用在复杂的类型中,所以后面更复杂的类型我们就不考虑多级指针了,最多只考虑一级指针. 

int p(int); //从P 处起,先与()结合,说明P 是一个函数,然后进入()里分析,说明该函数有一个整型变量的参数,然后再与外面的int 结合,说明函数的返回值是一个整型数据 

Int (*p)(int); //从P 处开始,先与指针结合,说明P 是一个指针,然后与()结合,说明指针指向的是一个函数,然后再与()里的int 结合,说明函数有一个int 型的参数,再与最外层的int 结合,说明函数的返回类型是整型,所以P 是一个指向有一个整型参数且返回类型为整型的函数的指针 

int *(*p(int))[3]; //可以先跳过,不看这个类型,过于复杂从P 开始,先与()结合,说明P 是一个函数,然后进入()里面,与int 结合,说明函数有一个整型变量参数,然后再与外面的*结合,说明函数返回的是一个指针,,然后到最外面一层,先与[]结合,说明返回的指针指向的是一个数组,然后再与*结合,说明数组里的元素是指针,然后再与int 结合,说明指针指向的内容是整型数据.所以P 是一个参数为一个整数据且返回一个指向由整型指针变量组成的数组的指针变量的函数.

既然指针变量都是用来存放地址的,那么C语言中为什么不直接设定一种通用型的指针变量,而是要指定基类型呢

void* 就是所谓的 通用型的指针了

对于这样的指针

void* p = NULL;

不能++p、--p、p+10、*p等等等等

因为这些操作全部是依赖指针指向的具体类型进行操作的

现在你有了一个单独存地址的指针了,然后你拿到这个地址有什么用呢?

3. 指针的算术运算

指针可以加上或减去一个整数。指针的这种运算的意义和通常的数值的加减运算的意义是不一样的,以单元为单位。例如:

char a[20];
int *ptr=(int *)a; //强制类型转换并不会改变a 的类型
ptr++;

在上例中,指针ptr 的类型是int*,它指向的类型是int,它被初始化为指向整型变量a。接下来的第3句中,指针ptr被加了1,编译器是这样处理的:它把指针ptr 的值加上了sizeof(int),在32 位程序中,是被加上了4,因为在32 位程序中,int 占4个字节。由于地址是用字节做单位的,故ptr 所指向的地址由原来的变量a 的地址向高地址方向增加了4 个字节。由于char 类型的长度是一个字节,所以,原来ptr 是指向数组a 的第0 号单元开始的四个字节,此时指向了数组a 中从第4 号单元开始的四个字节。我们可以用一个指针和一个循环来遍历一个数组,看例子:

int array[20]={0};
int *ptr=array;
for(i=0;i<20;i++)
{
    (*ptr)++;
    ptr++;
}

这个例子将整型数组中各个单元的值加1。由于每次循环都将指针ptr加1 个单元,所以每次循环都能访问数组的下一个单元。

char a[20]="You_are_a_girl"; 
int *ptr=(int *)a; 
ptr+=5;

在这个例子中,ptr 被加上了5,编译器是这样处理的:将指针ptr 的值加上5 乘sizeof(int),在32 位程序中就是加上了5 乘4=20。由于地址的单位是字节,故现在的ptr 所指向的地址比起加5 后的ptr 所指向的地址来说,向高地址方向移动了20 个字节。在这个例子中,没加5 前的ptr 指向数组a 的第0 号单元开始的四个字节,加5 后,ptr 已经指向了数组a 的合法范围之外了。虽然这种情况在应用上会出问题,但在语法上却是可以的。

int main() 
{ 
    char a[20]="You_are_a_girl"; 
    char *p=a; 
    char **ptr=&p; 
    printf("**ptr=%c\n",**ptr); 
    ptr++; 
    printf("**ptr=%c\n",**ptr); 
}

  • 理解1:

输出答案为Y 和o:ptr 是一个char 的二级指针,当执行ptr++;时,会使指针加一个sizeof(char)

  • 理解2:

输出答案为Y 和a:ptr 指向的是一个char *类型,当执行ptr++;时,会使指针加一个sizeof(char *)(有可能会有人认为这个值为1,那就会得到1的答案,这个值应该是4), 即&p+4; 那进行一次取值运算不就指向数组中的第五个元素了吗?那输出的结果不就是数组中第五个元素了吗?.

  • 理解3:

ptr 的类型是char *,指向的类型是一个char 类型,该指向的地址就是p的地址(&p),当执行ptr++;时,会使指针加一个sizeof(char),即&p+4;那(&p+4)指向哪呢,是一个随机的值,或许是一个非法操作.

总结一下:

一个指针ptrold 加(减)一个整数n 后,结果是一个新的指针ptrnew,ptrnew 的类型和ptrold 的类型相同,ptrnew 所指向的类型和ptrold所指向的类型也相同。ptrnew 的值将比ptrold 的值增加(减少)了n 乘sizeof(ptrold 所指向的类型)个字节。就是说,ptrnew 所指向的内存区将比ptrold 所指向的内存区向高(低)地址方向移动了n 乘sizeof(ptrold 所指向的类型)个字节。指针和指针进行加减:两个指针不能进行加法运算,这是非法操作,因为进行加法后,得到的结果指向一个不知所向的地方,而且毫无意义。两个指针可以进行减法操作,但必须类型相同,一般用在数组方面,

4. 函数指针

返回值为指针的函数

首先说明一种容易搞混的概念,即,返回值为指针的函数,本质是一个函数。

定义格式如下:函数类型 *函数名([参数列表])

当然,也可以让指针标志与函数类型紧贴在一起,与函数名分开,其含义一致,格式如下:函数类型 函数名([参数列表])

相比上一种,这种方式更能表示这是一个指针函数。在将指针函数与函数指针区分时,也可以通过“指针标志*能否和函数名分离”来判断这个一个指针函数,还是一个函数指针。

返回值问题:

指针函数的使用和一般函数的使用相同,但需注意返回值问题。总体原则是:返回的指针对应的内存空间不会因函数返回则被释放掉。常用的返回指针有以下几种:

(1)函数中动态分配内存空间(通过malloc等实现)的首地址;

(2)静态变量(static)或全局变量所对应的变量的首地址;

(3)通过指针形参所获得的实参的有效地址。

但总的来说,个人不推荐返回值采用指针,若需要传递指针,最好作为形参传入。

指向函数的指针

而真正的函数指针就是一个指向函数的指针。每个函数在编译时,会被分配一个入口地址,一般用函数名来表示,这个地址就是该函数的指针。

函数指针的定义格式如下:函数类型 (*指针变量) ([参数列表])

在形式上,函数指针的特征是使用一个括号包裹指针标志和指针变量,将括号移除,函数指针就变成指针函数。

之所以容易混淆指针函数和函数指针,是因为指针函数的形式如一般指针变量类型。

如:int *x、int *y()、int (*z)()

这里x和z表示一个指针,而y则表示一个函数,所以要注意区分指向变量的指针和指向函数的指针的形式区别。如下是一个函数指针:

int (*pf)(int,int);//未初始化

pf可指向int(int,int)类型的函数。pf前面有*,说明pf是指针,右侧是形参列表,表示pf指向的是函数,左侧为int,说明pf指向的函数返回值为int。则pf可指向int(int,int)类型的函数。

赋值:

针对

int add(int nLeft,int nRight);//函数定义

可使用以下语句赋值:

pf = add;//通过赋值使得函数指针指向某具体函数

使用函数名给指向函数的指针变量赋值。其赋值的一般格式如下:函数指针 = [ &] 函数名;

其中,函数名后不能带括号和参数,函数名前的&是可选。

调用:

函数指针调用格式:函数指针变量([实参列表]); 或 (*函数指针变量)([实参列表]);

比如pf(100,100);(*pf)(100,100);

推荐第二种用法。这种方法可以很好的表明这是一个函数。而第一种方法则很容易造成误导。

当把函数名作为一个值使用时,该函数自动的转换成指针,如:

pf = length_compare <=>等价于pf = &length_compare

使用场景:

函数指针的常见用途就是把函数指针作为参数传递给函数。

回调函数

一个函数通过由运行时决定的指针来调用另一个函数的行为叫做回调(callback)。用户将一个函数指针作为参数传递给其它函数,后者将“回调”用户的函数。这样就可实现通过同一接口实现对不同类型数据、不同功能的处理。

函数指针作为某个函数的参数来使用,回调函数就是一个通过函数指针调用的函数。

简单讲:回调函数是由别人的函数执行时调用你实现的函数。

以下是来自知乎作者常溪玲的解说:

你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。

实例:

实例中 populate_array 函数定义了三个参数,其中第三个参数是函数的指针,通过该函数来设置数组的值。

实例中定义了回调函数 getNextRandomValue,它返回一个随机值,它作为一个函数指针传递给 populate_array 函数。

populate_array 将调用 10 次回调函数,并将回调函数的返回值赋值给数组。

C与C++之指针(五)_第1张图片
image

实例2.

#include 
#include 
/****************************************
* 函数指针结构体
***************************************/
typedef struct _OP {
    float (*p_add)(float, float);
    float (*p_sub)(float, float);
    float (*p_mul)(float, float);
    float (*p_div)(float, float);
} OP;
/****************************************
* 加减乘除函数
***************************************/
float ADD(float a, float b)
{
    return a + b;
}
float SUB(float a, float b)
{
    return a - b;
}
float MUL(float a, float b)
{
    return a * b;
}
float DIV(float a, float b)
{
    return a / b;
}
/****************************************
* 初始化函数指针
***************************************/
void init_op(OP *op)
{
    op->p_add = ADD;
    op->p_sub = SUB;
    op->p_mul = &MUL;
    op->p_div = &DIV;
}
/****************************************
* 库函数
***************************************/
float add_sub_mul_div(float a, float b, float (*op_func)(float, float))
{
    return (*op_func)(a, b);
}
int main(int argc, char *argv[])
{
    OP *op = (OP *)malloc(sizeof(OP));
    init_op(op);
    /* 直接使用函数指针调用函数 */
    printf("ADD = %f, SUB = %f, MUL = %f, DIV = %f\n", (op->p_add)(1.3, 2.2), (*op->p_sub)(1.3, 2.2),
            (op->p_mul)(1.3, 2.2), (*op->p_div)(1.3, 2.2));
    /* 调用回调函数 */
    printf("ADD = %f, SUB = %f, MUL = %f, DIV = %f\n",
            add_sub_mul_div(1.3, 2.2, ADD),
            add_sub_mul_div(1.3, 2.2, SUB),
            add_sub_mul_div(1.3, 2.2, MUL),
            add_sub_mul_div(1.3, 2.2, DIV));

    return 0;
}

代码示例:MTcpServer.cpp

指向类成员函数的函数指针

定义:类成员函数指针(member function pointer),是 C++ 语言的一类指针数据类型,用于存储一个指定类具有给定的形参列表与返回值类型的成员函数的访问信息。

基本上要注意的有两点:

• 1、函数前面必须带有类名,如A::func,函数指针赋值要使用 &

• 2、使用 .* (实例对象)或者 ->*(实例对象指针)调用类成员函数指针所指向的函数

类成员函数指针与普通函数指针不是一码事。前者要用 .* 与 ->* 运算符来使用,而后者可以用 * 运算符(称为"解引用"dereference,或称"间址"indirection)。

普通函数指针实际上保存的是函数体的开始地址,因此也称"代码指针",以区别于 C/C++ 最常用的数据指针。

而类成员函数指针就不仅仅是类成员函数的内存起始地址,还需要能解决因为 C++ 的多重继承、虚继承而带来的类实例地址的调整问题,所以类成员函数指针在调用的时候一定要传入类实例对象。

实例:

1.  class Person 
2.  { 
3.  public: 
4.      /*这里稍稍注意一下,我将speak()函数设置为普通的成员函数,而hello()函数设置为虚函数*/ 
5.      int value; 
6.      void speak()     
7.      { 
8.          cout << "I am a person!" << endl; 
9.          printf ("%d\n", &Person::speak); *在这里验证一下,输出一下地址就知道了!*/ 
10.    } 
11.    virtual void hello() 
12.    { 
13.        cout << "Person say \"Hello\"" << endl; 
14.    } 
15.    Person() 
16.    { 
17.        value = 1; 
18.    } 
19. 
20\. }; 
21. 
22. 
23. 
24\. class Baizhantang: public Person 
25\. { 
26\. public: 
27.    void speak() 
28.    { 
29.        cout << "I am 白展堂!" << endl; 
30.    } 
31.    virtual void hello() 
32.    { 
33.        cout << "白展堂 say \"hello!\"" << endl; 
34.    } 
35.    Baizhantang() 
36.    { 
37.        value = 2; 
38.    } 
39. }; 
40. 
41. typedef void (Person::*p)();//定义指向Person类无参数无返回值的成员函数的指针 
42. typedef void (Baizhantang::*q)();//定义指向Baizhantang类的无参数无返回值的指针 
43. 
44\. int main() 
45\. { 
46.    Person pe; 
47.    int i = 1; 
48.    p ip; 

49.    ip = &Person::speak;    //ip指向Person类speak函数 

50.    (pe.*ip)();    //这个是正确的写法! 

51. 

52.    //-------------------------------------------- 

53.    //  result : I am a Person! 

54.    //          XXXXXXXXXX(表示一段地址) 

55.    //-------------------------------------------- 

56. 

57.    /*

58\.    *下面是几种错误的写法,要注意!

59\.    *      pe.*ip();

60\.    *      pe.(*ip)();

61\.    *      (pe.(*ip))();

62\.    */ 

63. 

64.    Baizhantang bzt; 

65.     

66.    q iq = (void (Baizhantang::*)())ip; //强制转换 

67.    (bzt.*iq)(); 

68. 

69.    //-------------------------------------------- 

70.    //  result : I am a Person! 

71.    //          XXXXXXXXXX(表示一段地址) 

72.    //-------------------------------------------- 

73. 

74.    /*  有人可能会问了:ip明明被强制转换成了Baizhantang类的成员函数的指针,为什么输出结果还是:

75\.    * I am a Person!在C++里面,类的非虚函数都是采用静态绑定,也就是说类的非虚函数在编译前就已经

76\.    *确定了函数地址!ip之前就是指向Person::speak函数的地址,强制转换之后,只是指针类型变了,里面

77\.    *的值并没有改变,所以调用的还是Person.speak函数,细心的家伙会发现,输出的地址都是一致的.

78\.    *这里要强调一下:对于类的非静态成员函数,c++编译器会给每个函数的参数添加上一个该类的指针this,这也

79\.    *就是为什么我们在非静态类成员函数里面可以使用this指针的原因,当然,这个过程你看不见!而对于静态成员

80\.    *函数,编译器不会添加这样一个this。

81\.    */ 

82.     

83.    iq = &Baizhantang::speak;  /*iq指向了Baizhantang类的speak函数*/ 

84.    ip = (void (Person::*)())iq;    /*ip接收强制转换之后的iq指针*/ 

85.    (bzt.*ip)(); 

86. 

87.    //-------------------------------------------- 

88.    //  result : I am 白展堂! 

89.    //-------------------------------------------- 

90. 

91.    (bzt.*iq)();//这里我强调一下,使用了动态联编,也就是说函数在运行是才确定函数地址! 

92. 

93.    //-------------------------------------------- 

94.    //  result : I am 白展堂! 

95.    //-------------------------------------------- 

96. 

97.    /*这一部分就没有什么好讲的了,很明白了!由于speak函数是普通的成员函数,在编译时就知道

98\.    *到了Baizhantang::speak的地址,因此(bzt.*ip)()会输出“I am 白展堂!”,即使iq被强制转换

99\.    *成(void (Person::*)())类型的ip,但是其值亦未改变,(bzt.*iq)()依然调用iq指向处的函数

100\.        *即Baizhantang::speak.

101\.        */ 

102.     

103.     

104.        /*好了,上面讲完了普通成员函数,我们现在来玩一点好玩的,现在来聊虚函数*/ 

105.        ip = &Person::hello;    /*让ip指向Person::hello函数*/ 

106.        (pe.*ip)(); 

107.     

108.        //-------------------------------------------- 

109.        //  result : Person say "Hello" 

110.        //-------------------------------------------- 

111.     

112.        (bzt.*ip)(); 

113.     

114.        //-------------------------------------------- 

115.        //  result : 白展堂 say "Hello" 

116.        //-------------------------------------------- 

117.     

118.        /*咦,这就奇怪了,为何与上面的调用结果不类似?为什么两个调用结果不一致?伙伴们注意了:

119\.        *speak函数是一个虚函数,前面说过虚函数并不是采用静态绑定的,而是采用动态绑定,所谓动态

120\.        *绑定,就是函数地址得等到运行的时候才确定,对于有虚函数的类,编译器会给我们添加一个指针

121\.        *vptr,指向一个虚函数表vptl,vptl里面存放着虚函数的地址,子类继承父类的时候,也会继承这样

122\.        *一个指针,如果子类复写了虚函数,那么该表中该虚函数地址将会由父类的虚函数地址替换成子类虚

123\.        *函数地址,编译器会把(pe.*ip)()转化成为(pe.vptr[1])(pe),加上动态绑定,结果会输出:

124\.        *      Person say "Hello"   

125\.        *(bzt.*ip)()会被转换成(bzt.vptr[1])(pe),自然会输出:

126\.        *      白展堂 say "Hello"

127\.        *ps:这里我没法讲得更详细,因为解释起来肯定是很长很长的,感兴趣的话,我推荐两本书你去看一看:

128\.        *  第一本是侯捷老师的<深入浅出MFC>,里面关于c++的虚函数特性讲的比较清楚;

129\.        *  第二本是侯捷老师翻译的<深度探索C++对象模型>,一听名字就知道,讲这个就更详细了;

130\.        *当然,不感兴趣的同学这段解释可以省略,对与使用没有影响!

131\.        */ 

132.     

133.        iq = (void (Baizhantang::*)())ip; 

134.        (bzt.*iq)(); 

135.     

136.        //-------------------------------------------- 

137.        //  result : 白展堂 say "Hello" 

138.        //-------------------------------------------- 

139.         

140.        system("pause"); 

141.        return 0; 

142.    } 

c++常用函数指针用法

std::function:

不同类型可能具有相同的调用形式,如:

// 普通函数
int add(int a, int b){return a+b;}
// lambda表达式
auto mod = [](int a, int b){ return a % b;}
// 函数对象类
struct divide{
    int operator()(int denominator, int divisor){
        return denominator/divisor;
    }
};

上述三种可调用对象虽然类型不同,但是共享了一种调用形式:

int(int ,int)

std::function就可以将上述类型保存起来,如下:

std::function  a = add;
std::function  b = mod ;
std::function  c = divide();

std::function 是一个可调用对象包装器,是一个类模板,可以容纳除了类成员函数指针之外的所有可调用对象,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟它们的执行。

定义格式:std::function<函数类型>

std::function可以取代函数指针的作用,因为它可以延迟函数的执行,特别适合作为回调函数使用。它比普通函数指针更加的灵活和便利。

std::bind:

可将std::bind函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。

std::bind将可调用对象与其参数一起进行绑定,绑定后的结果可以使用std::function保存。std::bind主要有以下两个作用:

  • 将可调用对象和其参数绑定成一个防函数;

  • 只绑定部分参数,减少可调用对象传入的参数。

std::bind绑定普通函数:

double my_divide (double x, double y) {return x/y;}
auto fn_half = std::bind (my_divide,_1,2); 
std::cout << fn_half(10) << '\n';                        // 5

bind的第一个参数是函数名,普通函数做实参时,会隐式转换成函数指针。因此std::bind (my_divide,_1,2)等价于std::bind (&my_divide,_1,2)

_1表示占位符,位于中,std::placeholders::_1

std::bind绑定一个成员函数


struct Foo {

    void print_sum(int n1, int n2)

    {

        std::cout << n1+n2 << '\n';

    }

    int data = 10;

};

int main()

{

    Foo foo;

    auto f = std::bind(&Foo::print_sum, &foo, 95, std::placeholders::_1);

    f(5); // 100

}

bind绑定类成员函数时,第一个参数表示对象的成员函数的指针,第二个参数表示对象的地址。

必须显示的指定&Foo::print_sum,因为编译器不会将对象的成员函数隐式转换成函数指针,所以必须在Foo::print_sum前添加&

使用对象成员函数的指针时,必须要知道该指针属于哪个对象,因此第二个参数为对象的地址 &foo;

5. 引用和指针的区别

C++primer中对 对象的定义:对象是指一块能存储数据并具有某种类型的内存空间

一个对象a,它有值和地址&a,运行程序时,计算机会为该对象分配存储空间,来存储该对象的值,我们通过该对象的地址,来访问存储空间中的值

指针p也是对象,它同样有地址&p和存储的值p,只不过,p存储的数据类型是数据的地址。如果我们要以p中存储的数据为地址,来访问对象的值,则要在p前加解引用操作符"",即p。

对象有常量(const)和变量之分,既然指针本身是对象,那么指针所存储的地址也有常量和变量之分,常量指针是指,指针这个对象所存储的地址是不可以改变的,而指向常量的指针的意思是,不能通过该指针来改变这个指针所指向的对象。

我们可以把引用理解成变量的别名。定义一个引用的时候,程序把该引用和它的初始值绑定在一起,而不是拷贝它。计算机必须在声明r的同时就要对它初始化,并且,r一经声明,就不可以再和其它对象绑定在一起了。

实际上,你也可以把引用看做是通过一个常量指针来实现的,它只能绑定到初始化它的对象上。

关于指针和引用的对比,可以参看<>中的第一条条款,引用的一个优点是它一定不为空,因此相对于指针,它不用检查它所指对象是否为空,这增加了效率

引用的创建和销毁不会调用类的拷贝构造函数和析构函数。

6. 结构体指针

对于指向结构体的指针,要使用内部的成员时,可以有以下两种方式:

(*指针变量名).成员名/指针变量名->成员名

思考:结构体中能否放函数?


#include 

struct DEMO 

{ 

    int x,y; 

    int (*func)(int,int); //函数指针 

}; 

int add1(int x,int y) 

{ 

    return x*y; 

} 

int add2(int x,int y) 

{ 

    return x+y; 

} 

void main() 

{ 

    struct DEMO demo; 

    demo.func=add2; //结构体函数指针赋值 

    //demo.func=&add2; //结构体函数指针赋值 

    printf("func(3,4)=%d\n",demo.func(3,4)); 

    demo.func=add1; 

    printf("func(3,4)=%d\n",demo.func(3,4)); 

}

执行后终端显示:


func(3,4)=7

func(3,4)=12

6.1 内存对齐

对齐原则:

  • 原则1:数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。

  • 原则2:结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。

  • 原则3:结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。

默认对齐值:

Linux 默认#pragma pack(4)

window 默认#pragma pack(8)

注:可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是指定的“对齐系数”。

例一:一字节对齐

第一步: 成员数据对齐

C与C++之指针(五)_第2张图片
image

第二步: 整体对齐

整体对齐系数 = min((max(int,short,char), 1) = 1,所以不需要再进行整体对齐。整体大小就为8。

图示如下:

C与C++之指针(五)_第3张图片
image

例二:二字节对齐

第一步: 成员数据对齐

第二步: 整体对齐

整体对齐系数 = min((max(int,short,char), 2) = 2,将9提升到2的倍数,则为10.所以最终结果为10个字节。

图示如下:(X为补齐部分)

C与C++之指针(五)_第4张图片
image

例三:四字节对齐

第一步: 成员数据对齐

第二步: 整体对齐

整体对齐系数 = min((max(int,short,char), 4) = 4,将9提升到4的倍数,则为12.所以最终结果为12个字节。

图示如下:(X为补齐部分)

C与C++之指针(五)_第5张图片
image

例三:八字节对齐

第一步: 成员数据对齐

第二步: 整体对齐

整体对齐系数 = min((max(int,short,char), 8) = 4,将9提升到4的倍数,则为12.所以最终结果为12个字节。图示如上。

注:可以通过stddef.h库中的offsetof宏来查看对应结构体元素的偏移量。

例四:结构体中包含结构体

整体计算过程如下

C与C++之指针(五)_第6张图片
image

图示如下:

C与C++之指针(五)_第7张图片
image

例五:再来一个嵌套结构体的计算

整体计算过程如下

C与C++之指针(五)_第8张图片
image

图示如下:

C与C++之指针(五)_第9张图片
image

小结:当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。

会了关于结构体内存大小的计算,可是为什么系统要对于结构体数据进行内存对齐呢,很明显所占用的空间大小要更多。原因可归纳如下:

1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

更简单的说明下:如图

C与C++之指针(五)_第10张图片
image

首先,cpu的访问粒度为4,也就是一次性可以读取内存中的四个字节内容;当我们不采用内存对齐策略,如果需要访问A中的b元素,cpu需要先取出03四个字节的内容,发现没有读取完,还需要再次读取,一共需要进行两次访问内存的操作;而有了内存对齐,参考左图,可一次性取出47四个字节的元素也即是b,这样就只需要进行一次访问内存的操作。所以操作系统这样做的原因也就是所谓的拿空间换时间,提高效率。

建议:虽然操作系统会浪费空间来完成内存对齐,但是我们有了上面的知识可以通过按照数据类型来调整结构体内部的数据的先后顺序来尽量减少内存的消耗;例如我们将下面结构体A中的顺序调整为B,sizeof(A)的结果为12,而sizeof(B)的结果就是8:

C与C++之指针(五)_第11张图片
image

6.2 指针成员

当结构体中有指针变量的时候,切记要将这个成员malloc,否则编译不会出错,但是运行的时候程序会跑飞了。

示例1:

C与C++之指针(五)_第12张图片
image

运行结果正常。 结构体A占据的内存空间是8字节(32Bit操作系统),int型变量和char型变量各占据4字节,strlen(ptr)等于11字节,所以malloc分配的空间是20字节,分配出来的空间是地址连续的堆空间。执行memcpy()时候,ptr字符串内容会覆盖结构体中dat数组变量的起始空间,而后续的空间又有(strlen(ptr) + 1)大小,所以运行正常。

示例2:

如果把结果体A中的数组变量dat改成指针变量*dat

C与C++之指针(五)_第13张图片
image

显然,运行后发生Segmentation fault。这是因为malloc是在堆内存分配空间的,堆上的数据默认是0,也就是说指针变量默认是NULL。

在malloc之后打印结构体A的首地址以及type、dat的地址:

printf("a = %p, &a->type = %p, a->dat = %p\n", a, &a->type, a->dat);

运行:

18943463-f96c3af2236b6587.png
image

memcpy()函数访问的目标地址是0地址处,所以自然段错误。

如何修改,显然需要将a->dat指针指向一个合适的地方去。指向a->type之后的地址?

C与C++之指针(五)_第14张图片
image
C与C++之指针(五)_第15张图片
image

memcpy()操作的目的地址是0x8b4d00c,而结构体变量a的起始地址,也就是a结构体首个变量type的地址0x8b4d008,其中偏移4字节,跟前面示例1对比,看似并没什么问题,但是注意,示例1中a->type之后是一个数组变量,而在本例中,a->type之后是一个指针变量,它原指向NULL,经这么修改,它指向的是自己的地址了。指针变量也是变量,它的存在本身也需要地址存放,每个指针都需要占据空间给自己用,memcpy()函数的是dat指针给自己生存用的地址,因此出现段错误。应修改为:

a->dat = (char* )(a + 1);

也就是说a->dat指向A之外的内存大小为strlen(ptr) + 1空间的起始地址,这样就可以正常运行。
(但是建议也不要这样赋值)

另一个例子:

1.  #include  
2.  struct str{ 
3.      int len; 
4.      char s[0]; 
5.  }; 
6.  struct foo { 
7.      struct str *a; 
8.  }; 
9.  int main(int argc, char** argv) { 
10.    struct foo f={0}; 
11.    if (f.a->s) { 
12.        printf( f.a->s); 
13.    } 
14.    return 0; 
15. } 

编译一下上面的代码,在VC++和GCC下都会在14行的printf处crash掉你的程序。接下来,你调试一下,或是你把14行的printf语句改成:
printf("%x\n", f.a->s);
你会看到程序不crash了。程序输出:4。 这下你知道了,访问0x4的内存地址,不crash才怪。于是,你一定会有如下的问题:

1)为什么不是13行if语句出错?f.a被初始化为空了嘛,用空指针访问成员变量为什么不crash?

2)为什么会访问到了0x4的地址?4是怎么出来的?

3)代码中的第4行,char s[0] 是个什么东西?零长度的数组?为什么要这样玩?

让我们从基础开始一点一点地来解释C语言中这些诡异的问题。

结构体中的成员:

首先,我们需要知道——所谓变量,其实是内存地址的一个抽像名字罢了。在静态编译的程序中,所有的变量名都会在编译时被转成内存地址。机器是不知道我们取的名字的,只知道地址。

所以有了——栈内存区,堆内存区,静态内存区,常量内存区,我们代码中的所有变量都会被编译器预先放到这些内存区中。

1.  struct test{ 
2.      int i; 
3.      short c; 
4.      char *p; 
5.  }; 
6.  int main(){ 
7.      struct test *pt=NULL; 
8.      return 0; 
9.  } 

编译后,我们用gdb调试一下,当初始化pt后,我们看看如下的调试:(我们可以看到就算是pt为NULL,访问其中的成员时,其实就是在访问相对于pt的内址)

1.  (gdb) p pt 
2.  $1 = (struct test *) 0x0 
3.  (gdb) p pt->i 
4.  Cannot access memory at address 0x0 
5.  (gdb) p pt->c 
6.  Cannot access memory at address 0x4 
7.  (gdb) p pt->p 
8.  Cannot access memory at address 0x8 

注意:上面的pt->p的偏移之所以是0x8而不是0x6,是因为内存对齐了(在64位系统上)。

现在知道为什么原题中会访问到了0x4的地址了,因为是相对地址。

相对地址有很好多处,其可以玩出一些有意思的编程技巧,比如把C搞出面向对象式的感觉来,可以参看11年前的文章《用C写面向对像的程序》(用指针类型强转的危险玩法——相对于C++来说,C++编译器帮你管了继承和虚函数表,语义也清楚了很多)

指针和数组的差别:

有了上面的基础后,你把源代码中的struct str结构体中的char s[0];改成char *s;试试看,会发现,在13行if条件的时候,程序因为Cannot access memory就直接挂掉了。为什么声明成char s[0],程序会在14行挂掉,而声明成char *s,程序会在13行挂掉?char *schar s[0]有什么差别呢?

这是因为,访问成员数组名其实得到的是数组的相对地址,而访问成员指针其实是相对地址里的内容(这和访问其它非指针或数组的变量是一样的)

换句话说,对于数组 char s[10]来说,数组名s&s都是一样的(不信你可以自己写个程序试试)。在我们这个例子中,也就是说,都表示了偏移后的地址。这样,如果我们访问 指针的地址(或是成员变量的地址),那么也就不会让程序挂掉了。

正如下面的代码,可以运行一点也不会crash掉

1.  struct test{ 
2.      int i; 
3.      short c; 
4.      char *p; 
5.      char s[10]; 
6.  }; 
7.  int main(){ 
8.      struct test *pt=NULL; 
9.      printf("&s = %x\n", pt->s); //等价于 printf("%x\n", &(pt->s) ); 
10.    printf("&i = %x\n", &pt->i); //因为操作符优先级,我没有写成&(pt->i) 
11.    printf("&c = %x\n", &pt->c); 
12.    printf("&p = %x\n", &pt->p); 
13.    return 0; 
14\. } 

  • 关于零长度的数组:
    首先,我们要知道,0长度的数组在ISO C和C++的规格说明书中是不允许的。那么为什么gcc可以通过而连一个警告都没有?那是因为gcc 为了预先支持C99的这种玩法,所以,让“零长度数组”这种玩法合法了。关于GCC对于这个事的文档在这里:“Arrays of Length Zero”,文档中给了一个例子:
1.  #include  
2.  #include  
3.  struct line { 
4.    int length; 
5.    char contents[0]; // C99的玩法是:char contents[]; 没有指定数组长度 
6.  }; 
7.  int main(){ 
8.      int this_length=10; 
9.      struct line *thisline = (struct line *) 
10.                      malloc (sizeof (struct line) + this_length); 
11.    thisline->length = this_length; 
12.    memset(thisline->contents, 'a', this_length); 
13.    return 0; 
14\. } 

上面这段代码的意思是:我想分配一个不定长的数组,于是我有一个结构体,其中有两个成员,一个是length,代表数组的长度,一个是contents,代码数组的内容。后面代码里的 this_length(长度是10)代表是我想分配的数据的长度。(这看上去是不是像一个C++的类?)这种玩法英文叫:Flexible Array,中文翻译叫:柔性数组。

如果你sizeof(char[0])或是 sizeof(int[0]) 之类的零长度数组,你会发现sizeof返回了0,这就是说,零长度的数组是存在于结构体内的,但是不占结构体的size。你可以简单的理解为一个没有内容的占位标识,直到我们给结构体分配了内存,这个占位标识才变成了一个有长度的数组。

看到这里,你会说,为什么要这样,把contents声明成一个指针,然后为它再分配一下内存不行么?就像下面一样。

1.  struct line { 
2.    int length; 
3.    char *contents; 
4.  }; 
5.  int main(){ 
6.      int this_length=10; 
7.      struct line *thisline = (struct line *)malloc (sizeof (struct line)); 
8.      thisline->contents = (char*) malloc( sizeof(char) * this_length ); 
9.      thisline->length = this_length; 
10.    memset(thisline->contents, 'a', this_length); 
11.    return 0; 
12\. } 

这不一样清楚吗?而且也没什么怪异难懂的东西。是的,这也是普遍的编程方式,代码是很清晰,也让人很容易理解。既然这样,那为什么要搞一个零长度的数组?

这个事情出来的原因是——我们想给一个结构体内的数据分配一个连续的内存!这样做的意义有两个好处:

  • 第一个意义是,方便内存释放。如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。(读到这里,你一定会觉得C++的封闭中的析构函数会让这事容易和干净很多)
  • 第二个原因是,这样有利于访问速度。连续的内存有益于提高访问速度,也有益于减少内存碎片。

我们来看看是怎么个连续的,用gdb的x命令来查看:(我们知道,用struct line {}中的那个char contents[]不占用结构体的内存,所以,struct line就只有一个int成员,4个字节,而我们还要为contents[]分配10个字节长度,所以,一共是14个字节)

1\.  (gdb) x /14b thisline 
2\.  0x601010:      10      0      0      0      97      97      97      97 
3\.  0x601018:      97      97      97      97      97      97 

从上面的内存布局我们可以看到,前4个字节是 int length,后10个字节就是char contents[]

如果用指针的话,会变成这个样子:

1.  (gdb) x /16b thisline 
2.  0x601010:      1      0      0      0      0      0      0      0 
3.  0x601018:      32      16      96      0      0      0      0      0 
4.  (gdb) x /10b this->contents 
5.  0x601020:      97      97      97      97      97      97      97      97 
6.  0x601028:      97      97 

上面一共输出了四行内存,其中,

• 第一行前四个字节是int length,第一行的后四个字节是对齐。

• 第二行是char* contents,64位系统指针8个长度,他的值是0x20 0x10 0x60 也就是0x601020

• 第三行和第四行是char* contents指向的内容。

从这里,我们看到,其中的差别——数组的原地址就是内容,而指针的那里保存的是内容的地址。

7. C++智能指针

智能指针和普通指针的区别在于智能指针实际上是对普通指针加了一层封装机制,这样的一层封装机制的目的是为了使得智能指针可以方便的管理一个对象的生命期。

在C++中,我们知道,如果使用普通指针来创建一个指向某个对象的指针,那么在使用完这个对象之后我们需要自己删除它,例如:

ObjectType* temp_ptr = new ObjectType();
temp_ptr->foo();
delete temp_ptr;

很多材料上都会指出说如果程序员忘记在调用完temp_ptr之后删除temp_ptr,那么会造成一个悬挂指针(dangling pointer),也就是说这个指针现在指向的内存区域其内容程序员无法把握和控制,也可能非常容易造成内存泄漏。

可是事实上,不止是“忘记”,在上述的这一段程序中,如果foo()在运行时抛出异常,那么temp_ptr所指向的对象仍然不会被安全删除。

在这个时候,智能指针的出现实际上就是为了可以方便的控制对象的生命期,在智能指针中,一个对象什么时候和在什么条件下要被析构或者是删除是受智能指针本身决定的,用户并不需要管理。

根据具体的条件,我们一般会讨论这样几种智能指针,而如下所说的这些智能指针也都是在boost library里面定义的 (http://www.boost.org/doc/libs/1_50_0/libs/smart_ptr/smart_ptr.htm):

1) scoped_ptr(unique_ptr):

这是比较简单的一种智能指针,正如其名字所述,scoped_ptr所指向的对象在作用域之外会自动得到析构,一个例子是:http://www.boost.org/doc/libs/1_50_0/libs/smart_ptr/scoped_ptr.htm

此外,scoped_ptrnon-copyable的,也就是说你不能去尝试复制一个scoped_ptr的内容到另外一个scoped_ptr中,这也是为了防止错误的多次析构同一个指针所指向的对象。

2) shared_ptr:

很多人理解的智能指针其实是shared_ptr这个范畴。

shared_ptr中所实现的本质是引用计数(reference counting),也就是说shared_ptr是支持复制的,复制一个shared_ptr的本质是对这个智能指针的引用次数加1,而当这个智能指针的引用次数降低到0的时候,该对象自动被析构,这一点两位同学给的答案都非常精彩,不再赘述。

需要特别指出的是,如果shared_ptr所表征的引用关系中出现一个环,那么环上所述对象的引用次数都肯定不可能减为0那么也就不会被删除。

(1)如果程序要使用多个指向同一个对象的指针,应选择shared_ptr。这样的情况包括:
(2)如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。如果函数使用new分配内存,并返还指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。这样,所有权转让给接受返回值的unique_ptr,而该智能指针将负责调用delete

使用:

不能自动将指针转换为智能指针对象,必须显式调用:

shared_ptr pd;
double *p_reg = new double;
pd = p_reg; // not allowed (implicit conversion)
pd = shared_ptr(p_reg); // allowed (explicit conversion)
shared_ptr pshared = p_reg; // not allowed (implicit conversion)
shared_ptr pshared(p_reg); // allowed (explicit conversion)

对全部智能指针都应避免的一点:

string vacation("I wandered lonely as a cloud.");
shared_ptr pvac(&vacation); // No

pvac过期时,程序将把delete运算符用于非堆内存,这是错误的。

8. 需要注意的点

野指针

多重指针

越界访问

Sizeof(指针)

用指针传递仍然无法修改值

用指针传递仍然无法修改值

void GetMemory( char*p )

{

p = (char*) malloc( 100 );

}

void Test( void )

{

char*str = NULL;

GetMemory( str );

strcpy( str, "hello world" );

printf( str );

}

Double free
在free一个变量时,要记得判断是否为空。

原文链接:https://www.jianshu.com/p/6cfc27a0cca5
阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680

你可能感兴趣的:(C与C++之指针(五))