系列文章目录

目录

part 1 基础部分

一.内存结构与指针

1.指针,指针变量,自由存储空间

2.  *的位置与两个作用

二.指针变量的类型与解读方式

1.定义,声明

       2.指针的声明和初始化

       3.一级万能指针

三.new,数组,指针算数

1.存储方案和动态分配(new)

2.指针数组

3.细谈数组名和指针的关系&&指针算数

4.数组指针与二维数组

5.多维数组的物理储存方式

6.字符串与指针

7.cv-限定

8.对数组名取地址

四.函数与指针,结构体与指针

1. 函数与一维数组

2.函数与二维数组

3.1 基础函数指针

3.2 深入函数指针

4.1 结构体对齐规则

4.2 结构体指针变量,c++类与结构体

五.this指针,虚基指针

1.this指针

       1.2.effective c++

2.虚函数原理简介

part 2 深入部分

六. 语意学

   

    1.执行语意学

       1.1. 再谈new和delete

    2.fuction语意学

        1.1 虚拟成员函数

        1.2 多重继承下的虚函数

        1.3 虚继承下的虚函数

    3.C, S, and D语意学

        1.1 pure虚函数

1.2 虚拟规格

        1.3 虚拟规格中的const


声明:大学基础达不到1200页国外编程书籍不用读第二部分,此部分由作者好友所写,不属于基础部分。



part 1 基础部分

一、二.内存结构与指针

1.内存编址与自由存储空间

计·算机的内存是一块用于存储数据的空间,由一系列连续的存储单元组成,就像下面这样,

bit

每一个最小单元格都表示 1 个 bit,1个bit 可以表示0或1两种状态,规定 8个 bit 为一组,命名为 byte(字节)

并且将 byte(字节) 作为内存寻址的最小单元,同时给每个 byte 一个唯一的编号,这个编号就叫内存的地址(虚拟地址),如下:

上面我们说给内存中每个 byte 唯一的编号,编号本身是一个十六进制的整数(一般从零开始),编号的范围就决定了计算机寻址内存的范围。这和平时常说的电脑是 32 位还是 64 位有关。16 位意味着最多对 2^16byte = 64 Kb 的内存编号寻址,而现在的电脑一般都是32位起步,32 位意味着可寻址的内存范围是 2^32 byte = 4GB。

此处简化c++的内存模型和存储性以及链接性的问题,做以下演示:

有了内存,接下来需要考虑的是int、double 这些变量是如何存储在内存里面的
在 C 语言中我们会这样定义变量:

int num = 999;
int char = 'c';

当你写下一个变量定义的时候,实际上是向内存申请了一块空间来存放你的变量。

我们都知道 int 类型占 4 个字节,并且在计算机中数字都是用补码储存的(非负数的补码是它的二进制表示,负数的补码是将它二进制表示的每个位都按位取反再加一的结果,这样做的意义稍后再做具体阐释)

999换算成补码就是: 0000 0011 1110 0111

这里有 4 个byte,所以需要四个单元格来存储:


注意到,我们把高位的字节放在了低地址的地方。

那能不能反过来呢?

由此引出大端和小端

像上面这种将高位字节放在内存低地址的方式叫做大端

反之,将低位字节放在内存低地址的方式就叫做小端,如图:

 大小端由CPU决定,但编译器也能指定编译模式为小端

上面说明了 int 型的变量如何存储在内存,而 float、char 等类型实际上也是一样的,都需要先转换为补码。

对于多字节的变量类型,还需要按照大端或者小端的格式,依次将字节写入到内存单元。

记住上面这两张图,这就是编程语言中所有变量的在内存中的样子,不管是 int、char、指针、数组、结构体、对象... 都是这样放在内存的。

2.指针和指针变量的概念

计算机程序在存储数据时必须跟踪数据的三种基本属性即:

#1信息存储在何处
#2存储的值为多少
#3存储的信息是什么类型

先来看下面的例子

想知道变量到底放在哪里可以通过运算符 & 来取得变量实际的地址,这个值就是变量所占内存块的起始地址,我们平时所说的地址都是起始地址,我们可以把这个地址打印出来:
printf("%p",&num);大概会是像这样的一串十六进制数字: 0x7ff83aab8f3b

那么如何存储这个地址,又如何将其标识为地址而不是普通的整数呢

我们引入一个新的概念,指针:

指针就是地址,地址就是指针:

C语言中系统为每一个内存单元分配一个地址值,c/c++将这个地址值称为指针,如:int a= 6;则存放变量a的内存单元的地址 &a被称为指针

指针变量:指针变量是存放前述指针(地址)的变量。指针变量就是存放指针(地址)的变量,指针变量是变量,存储的数据类型是指针(地址),p(指针变量)保存了...的地址就说p指向了...

例如我们定义int *p = #
p是指针,因为它是num所占连续空间的地址

p也是指针变量,因为它是变量而且变量的类型是地址(指针)

可以说,指针变量不止是指针,而且是指向特定类型的指针

值得庆幸的是,指针和指针变量一般在讨论时不会做具体区分,此处解决#1的问题。

3.指针的声明和初始化

1.定义,声明

1.单定义规则
C++有“单定义规则”(One DefinitionRule,ODR),该规则指出,变量只能有一次定义。为满足这种需求,C++提供了两种变量声明。一种是定义声明(defining declaration)或简称为定义(definition),它给变量分配存储空间;另一种是引用声明(referencing declaration)或简称为声明(declaration),它不给变量分配存储空间,因为它引用已有的变量。引用声明使用关键字extern,且不进行初始化;否则,声明为定义,导致分配存储空间,平时所说的变量声明一般是指定义声明。

1.指针变量的声明

声明步骤(声明的时候)
*修饰指针变量p。
保存谁的地址就先声明谁。

从上往下整体替换。

案例1:
声明一个指针变量p保存 int num的地址;int *p;

保存数组int arr[5]首地址;int (*p)[5]                        //[]的优先级高于*,所以加小括号
保存函数的入口地址 int fun(int,int);   int (*p)(int,int);

保存结构体变量的地址struct stu lucy;    struct stu *p;

保存指针变量int *q的地址int **p
note:在32位平台任何类型的指针变量都是4字节,即一个指针变量存储指针(地址)用的空间是32/8=4byte

1.1指针变量和普通变量建立关系

int num = 10;

int *p;

p = #

printf("%d",*num)  //结果是10

printf("%p",num)    //结果是num的首地址,16进制的一串数


*的第一个作用那就是:在定义和初始化指针时,*号仅起一个标识符的作用,告诉系统这是个指针而不是十六进制的普通数字,但是通常并不建议用以上的方式对一个指针变量进行赋值一方面是因为没有在刚开始定义的时候给定p的值,系统会自动给其分配一个值,如果这个值刚好是无法操作的内存部分的地址,就会引发段错误。通常将上述两个语句合二为一,也就是接下来的部分:

2.声明和初始化指针

我们前面已经讲过了如何声明一个指针,同时我们可以也在声明语句中初始化指针,在这种情况下,*仍然仅使起标识符的作用,也就是说被初始化的是指针,而不是它指向的值,例如下面语句是将pt(而不是*pt)的值设置为&higgens:

int higgens = 5;
int * pt = &higgens;

而这种情况下*的位置可以是任意的,它可以靠近前面的int,也可以靠近后面的pt,甚至放在中间也没关系,但是注意,*只作用于离它最近的一个指针变量,也就是说,如果你想同时定义两个指针变量,你应该写成如下形式:

typename *pointname1 ,  *pointname2     // 翻译:类型名 指针名1 指针名2

3. *号的第二个作用与指针类型

我们现在已经获得了变量的地址,可是仅仅变量的位置是不够的,我们需要通过这个地址访问甚至改写其指向的值,引出*号的两个作用:
在声明和初始化时,起标识符的作用,在使用指针(地址)时,起解引用的作用

编译器会根据指针的所指元素的类型去判断应该取多少个字节,找到指针变量所储存的地址在内存中的位置,然后提取相应的字节数,再按照对应类型的翻译方式进行翻译,如果是 int 型的指针,那么编译器就会产生提取四个字节的指令,char 则只提取一个字节,以此类推。

int num = 0x01020304(这里是普通数字,不是地址!)

如图,num作为int变量储存在4个字节中,也可表示为

0100 0011 0010 0001

的形式,p1,p2,p3都指向num的首地址,进行解引用时从num首地址开始分别取int(4byte) short(2byte) char(1byte) 的字节大小按照各自翻译方式进行翻译

int higgens = 5;
int * pt = &higgens;
printf("%d/n", * pt);
*pt = 10;
printf("%d/n", &higgens);
printf("%p/n", pt);
printf("%d/n", *pt);
printf("%d/n", higgens);

输出如下: 5    0x7ff83aab8f3b   0x7ff83aab8f3b   10  5                                                                我们可以将指针pt 和它指向的变量 higgens当成硬币的正反面,变量higgens代表值,并用&来取得地址, pt代表地址,并用*来取得值, 因此higgens和*pt完全等价,可以像用int变量那样来使用*pt,将值付给*pt将修改它指向的值,即higgens

指针指向的类型决定了翻译时读取的字节长度和翻译方式,而强制类型转换改变的仅仅是翻译时翻译方式和读取字节长度,不影响指针指向(指针所保存的地址)这个点会在指针算数的时候一并提及。

这里给出一个判断类型的小技巧,要看谁的类行只需要在声明语句(声明与初始化同时进行的时候指的是等号左边)中将要要判断的部分去掉,剩下的就是就是它的类型,例如:float *p 中p的类型是 float *(或者说p的类型是指向float的指针),*p的类型是float, int (*p)[5]中,p的类型是int * [5],即含一次取五个int的行指针;类型决定了翻译时读取的字节长度和翻译方式.

4.一级万能指针

1.void 不能定义普通变量

void num;  //wrong(c++)

2.void 可以定义指针变量
void *p;//ok p自身类型为void *,在32为平台任意类型的指针 为4byte
那么系统知道为p开辟4byte空间,所以定义成功

p就是万能的一级指针变量,能保存 任意一级指针的地址编号
int num = 10;
void *p = #
short data = 20;
p=&data
万能指钉一般用于*函数的形参达到操作多种数据类型的目的。
记住不要直接对void *p的指针变量取*,因为编译器不知道void取多长,更不知道怎么翻译,所以要先进行强制类型转换指明取字节数和翻译方式才能正确输出

int num = 10;

void    *     pi = #

std::cout<<   *(int *       )pi<<endl;  //输出pi的值     空格您随意

三、内存申请和指针算数

1.存储方案和动态分配

变量是在编译时分配的有名称的内存,我们前面所举的例子所用的指针只是为可以通过名称直接访问的内存提供了一个别名,指针的真正用武之地在于在运行阶段分配未命名的内存以储存值  c++用new来进行这种分配:

使用C++运算符new(或C函数malloc())分配的内存被称为动态内存。动态内存由运算符new和delete控制,而不是由作用域和链接性规则控制。因此,可以在一个函数中分配动态内存,而在另一个函数中将其释放。动态内存不是LIFO,其分配和释放顺序要取决于new和delete在何时以何种方式被使用。通常,编译器使用三块独立的内存:一块用于静态变量,一块用于自动变量,另外一块用于动态存储。
虽然存储方案概念不适用于动态内存,但适用于用来跟踪动态内存的自动和静态指针变量。例如,假设在一个函数中包含下面的语句:
float*p_fees= new float[20];
由new分配的80个字节(假设float为4个字节)的内存将一直保留在内存中,直到使用delete 运算符将其释放(但是如果有多个指针指向这块内存而只delete了一个指针,其它指针(地址)仍然是这一块内存的,再使用时往往会段错误,所以需要手动nullptr)。但当包含该声明的语句块执行完毕时,p_fees 指针将消失。如果希望另一个函数能够使用这80个字节中的内容,则必须将其地址传递或返回给该函数。另一方面,如果将pfees的链接性声明为外部的,则文件中位于该声明后面的所有函数都可以使用它。
注意:在程序结束时,由new分配的内存通常都将被释放,不过情况也并不总是这样。例如,在不那么规范的操作系统中,在某些情况下,请求大型内存块将导致该代码块在程序结束不会被自动释放。最佳的做法是,使用delete 来释放new分配的内存。

1.使用new运算符初始化
如果要初始化动态分配的变量,该如何办呢?在之前可以这样做,但C++11增加了其他可能性。下面先来看看之前的做法。
如果要为内置的标量类型(如int或double)分配存储空间并初始化,可在类型名后面加上初始值,并将其用括号括起:
int *pi = new int (6); // *pi set to 6
double *pd = new double (99.99);//*pd set to 99.99
这种括号语法也可用于有合适构造函数的类,然而,要初始化常规结构或数组,需要使用大括号的列表初始化,这要求编译器支持 C++11:

struct where {double x;double y;double z;};
where * one = new where {2.5,5.3,7.2};// C++11
int * ar = new int [4] (2,4,6,7};/ / C++11

在C++11中,还可将列表初始化用于单值变量:
int *pin = new int {6}; // *pin set to 6
double* pdo= new double{99.99};//* pd set to 99.99

2. new失败时
new可能找不到请求的内存量。在最初的10年中,C++在这种情况下让new返回空指针,但现在将引发异常 std::bad_alloc

3. new:运算符、函数和替换函数
运算符new和new []分别调用如下函数:
void* operator new( std:: size t);// used by new
void *operator new[](std::size t);//used by new[]
这些函数被称为分配函数(alloction function),它们位于全局名称空间中。同样,也有由delete和delete[]调用的释放函数(deallocation function):
void operator delete(void *);
void operator delete[](void *);

它们使用运算符重载语法 std::size_t是一个 typedef,对应于合适的整型。对于下面这样的基本语句:
int *pi =new int;
将被转换为下面这样:
int *pi =new(sizeof(int));
而下面的语句:
int * pa = new int[40];
将被转换为下面这样:
int *pa = new(40*sizeof(int));
使用运算符new的语句也可包含初始值,因此,使用new运算符时,可能不仅仅是调用new()函数(part2有深入的讨论)。同样,下面的语句:
delete pi;
将转换为如下函数调用:
delete (pi);
C++将这些函数称为可替换的(replaceable)。这意味着如果您有足够的意愿,可为new和delete 提供替换函数,并根据需要对其进行定制。例如,可定义作用域为类的替换函数,并对其进行定制,以满足该类的内存分配需求。在代码中,仍将使用new运算符,但它将调用您定义的new()函数。

4.定位new运算符
通常,new 负责在堆(heap)中找到一个足以能够满足要求的内存块。new运算符还有另一种变体,被称为定位(placement)new运算符,它让您能够指定要使用的位置。可以使用这种特性来设置其内存管理规程、处理需要通过特定地址进行访问的硬件或在特定位置创建对象。
要使用定位new特性,首先需要包含头文件new,它提供了这种版本的new运算符的原型;然后将new运算符用于提供了所需地址的参数。除需要指定参数外,句法与常规new运算符相同。具体地说,使用定位new 运算符时,变量后面可以有方括号,也可以没有。下面的代码段演示了new运算符

#include <new>
struct chaff
{
char dross [20];
int slag;
} ;
char buffer1[50];
char buffer2[500];
int main()
{
chaff *p1,*p2;
int *p3,*p4;
//first,the regular forms of new
p1 =new chaff;//place structure in heap
p3 =new int[20];//place int array in heap
//now,the two forms of placement new
p2 =new (buffer1)chaff;//place structure in buffer1
p4 =new (buffer2)int[20];//place int array in buffer2

出于简化的目的,这个示例使用两个静态数组来为定位new 运算符提供内存空间。因此,上述代码从buffer1中分配空间给结构 chaff,从buffer2中分配空间给一个包含20个元素的 int 数组。
熟悉定位new运算符后来看一个示例程序,如下程序使用常规new运算符和定位new 运算符创建动态分配的数组。该程序说明了常规new运算符和定位new运算符之间的一些重要差别,在查看该程序的输出后,将对此进行讨论。

// cpp--using placement new
#include <iostream>
#include <new>
const int BUF =512;
const int N =5;
char buffer[BUF];
int main()
{
using namespace std;
double *pd1,*pd2;
int i;
cout <<"Calling new and placement new:\n";
pdl =new double [N];                             //use heap(堆)
pd2 =new (buffer)double [N];                    //use buffer array
for ( i = 0; i < N; i++)  pd2[i] = pd1[i] = 1000 + 20.0 * i;
cout <<"Memory addresses:\n"<<"heap:"<<pd1
<<"static:"<<(void *)buffer <<endl;
cout <<"Memory contents:\n";
for (i = 0;i < N;i++)
{
cout << pdl[i] << " at " < < &pd1[i] << ";"; cout << pd2[i] < < " at " < < &pd2[i] << endl;
}
cout <<"\nCalling new and placement new a second time:\n"; double *pd3,*pd4;
pd3=new double[N];                                     //find new address
pd4 =new (buffer)double[N];//overwrite old data for ( i = 0;i < N;i++)
pd4[i] = pd3[i] = 1000 + 40.0 * i;
cout <<"Memory contents:\n";
for ( i = 0;i < N;i++)
{
cout << pd3[i] << " at " < < &pd3[i] << ";"; cout << pd4[i] << " at "<<&pd4[i]<< endl;
}
cout <<"\nCalling new and placement new a third time:\n"; delete []pdl;
pd1=new double [N];
pd2 =new (buffer +N *sizeof(double))double [N];
for ( i = 0;i < N;i++)
pd2[i] = pd1[i] = 1000 + 60.0 * i;
cout <<"Memory contents:\n";
for ( i = 0;i < N;i++)
{
cout < < pd1[i] < < " at " < < &pd1[i] < < ";";
cout << pd2[i] < < " at " < < &pd2[i] << endl;
}
delete []pdl;
delete [] pd3;
return 0;
}下面是该程序在某个系统上运行时的输出:
Calling new and placement new:
Memory addresses:
heap:006E4ABO static:00FD9138
Memory contents:
1000 at 006E4AB0;1000 at 00FD9138
1020 at 006E4AB8;1020 at 00FD9140
1040 at 006E4AC0;1040 at 00FD9148
1060 at 006E4AC8;1060 at 00FD9150
1080 at 006E4AD0;1080 at 00FD9158
Calling new and placement new a second time: Memory contents:
1000 at 006E4B68;1000 at 00FD9138
1040 at 006E4B70;1040 at 00FD9140
1080 at 006E4B78;1080 at 00FD9148
1120 at 006E4B80;1120 at 00FD9150
1160 at 006E4B88;1160 at 00FD9158
Calling new and placement new a third time: Memory contents:
1000 at 006E4AB0;1000 at 00FD9160
1060 at 006E4AB8;1060 at 00FD9168
1120 at 006E4AC0;1120 at 00FD9170
1180 at 006E4AC8;1180 at 00FD9178
1240 at 006E4AD0;1240 at 00FD9180

首先要指出的一点是,定位new运算符确实将数组p2放在了数组buffer中,p2和buffer的地址都是00FD9138。然而,它们的类型不同,p1是double指针,而buffer是char指针(顺便说一句,这也是程序使用(void*)对buffer进行强制转换的原因,如果不这样做,cout将显示一个字符串。同时,常规new将数组p1放在很远的地方,其地址为006E4AB0,位于动态管理的堆中。
需要指出的第二点是,第二个常规new运算符查找一个新的内存块,其起始地址为006E4B68;但第二个定位new运算符分配与以前相同的内存块:起始地址为00FD9138的内存块。定位new运算符使用传递给它的地址,它不跟踪哪些内存单元已被使用,也不查找未使用的内存块。这将一些内存管理的负担交给了程序员。例如,在第三次调用定位new运算符时,提供了一个从数组buffer开头算起的偏移量,因此将分配新的内存:
pd2 =new (buffer +N *sizeof(double)) double[N];//offset of 40 bytes 第三点差别是,是否使用delete来释放内存。对于常规new运算符,下面的语句释放起始地址为006E4AB0的内存块,因此接下来再次调用new运算符时,该内存块是可用的:
delete [] pd1;
然而,上例中的程序没有使用delete来释放使用定位new运算符分配的内存。事实上,在这个例子中不能这样做。buffer指定的内存是静态内存,而delete只能用于这样的指针:指向常规new运算符分配的堆内存。也就是说,数组buffer位于delete 的管辖区域之外,下面的语句将引发运行阶段错误:
delete [] pd2;//won't work

另外,如果buffer是使用常规new运算符创建的,便可以使用常规delete运算符来释放整个内存块。
定位new运算符的另一种用法是,将其与初始化结合使用,从而将信息放在特定的硬件地址处。
您可能想知道定位new运算符的工作原理。基本上,它只是返回传递给它的地址,并将其强制转换为void*,以便能够赋给任何指针类型。但这说的是默认定位new函数,C++允许重载定位new函数。

6.定位new的其他形式
就像常规new调用一个接收一个参数的new()函数一样,标准定位new 调用一个接收两个参数的new()函数:
int *pi =new int;                               //invokes new(sizeof(int))
int *p2 =new(buffer) int;                   //invokes new(sizeof(int),buffer)
int *p3 =new(buffer) int[40];            //invokes new(40*sizeof(int),buffer)
定位 new 函数不可替换,但可重载。它至少需要接收两个参数,其中第一个总是 std::size_t,指定了请求的字节数。这样的重载函数都被称为定义new,即使额外的参数没有指定位置。

2.指针数组

2.1 数组结构

在内存中,数组是一块连续的内存空间,第1个元素的地址称为数组的首地址,数组名实际就是数组首元素的地址,例如,我们定义一个数组:   int arr[5] = {10, 20, 30, 40, 50},其以这种方式储存:

2.2 指针数组

指针数组:本质是数组 只是数组的每个元素为指针

32位平台:
char *arrl[4];

short *arr2[4];

int *arr3[4];
sizeof(arrl) ==16byte

sizeof(arr2)==16byte

sizeof(arr3)==16byte

在声明语句中将数组名删除,剩下的就是数组的类型,也就是int * ,short*和char*,32位平台中任何类型的指针变量均占4字节,所以三者的数组大小都是4byte×4=16byte

int num1 = 10;
int num2 = 20;
int num3 = 30;
int num4 = 40;
int *arr[4] = {&numl, &num2, &num3, &num4};
int n = sizeof(arr)/sizeof(arr[0]);
int i;
for(i=0;i<n;i++){
cout<<*arr[i]<<"";//
}
cout<<endl;

对于上面的程序,最终输出的结果是10 20 30 40                                                                               那如果我们直接输出*arr呢?结果是&num1,这是因为我们的数组名就是&num1的指针(地址)我们通过一次取址得到了第一个元素,也就是*arr得到了&num1。注意,数组的类型是int *也就是说取址长度是int *对应的长度(32位平台4字节),所以如果有以下数组,那么结果是10,而不是整个数组: int arr[4] = {10, 20, 30, 40};  cout<<*arr;

3 细谈数组名和指针的关系 与指针算数

我们可以这样定义一个数组:

 int *psome = new int [10] {10,20,30,40};

我们用new开创了一个含有十个int变量的数组,并将数组的首地址返回给了psome,那么我们要如何访问数组中的元素呢,最简单的方法是只要把指针当数组名做使用即可,对于第一个元素,直接使用psome[0],第二个可以使用psome[1],以此类推,用指针来访问动态数组就很简单了,可以这样做的原因是在c和c++内部都使用指针来处理数组, 数组和指针基本等价是其优点之一,那么我们看如下的一个程序,它将指出指针和真正数组名之间的根本差别

#include <iostream>
int main(){
double * p3= new double [3]; // space for 3 doubles
p3[0] = 0.2;    // treat p3 like an array name
p3[1] =0.5;
p3[2]=0.8;
std::cout << "p3[1] is " << p3[1] <<".\n";
p3= p3+ 1:
// increment the pointer
std::cout Now p3[0] is " << p3[0] <<" and ";
std::cout <<"p3[1] is "<< p3[1] << ".\n";
p3= p3 -1;
delete [] p3;     // point back to beginning
return 0;
}

下面是该程序的输出:

p3[1] is 0.5.
Now p3[0] is 0.5 and p3[1] is 0.8.

由其可知P3仍旧是被当做数组名来使用的,下面的代码行指出了数组名和指针之间的根本差别:

p3 = p3 +1//指针可以,数组名不行                                                                                                    不能修改数组名的值,但指针是变量因此可以修改它的值,注意p3+1的效果,p3[0]现在是数组的第二个值,也就是说将p3加1以后,导致它指向第二个元素而不是第一个,将其减一后指针将指会原来的值,也就是说指针的加减是以指针指向的类型大小为单位的,对于int *p,p加减一地址会移动4个字节,也就是一个int的长度,同理,double的会移动一个double的长度,以此类推                还有一个区别是sizeof指针变量会输出4byte(32位),而sizeof数组名会输出数组大小。

4 数组指针与二维数组

可以认为二维数组以如图所示的方式储存

在二维数组中,每个元素arr[0],arr[1],arr[2]都是一块区域的名字,也就是一维数组名,即子数组的首地址,3个一维数组分别有4个元素。二维数组的数组名arr(注意区分这里的arr和arr[n])也是指针,指向的类型是int [4],可以理解为指向第一个子数组的数组指针,所以这里的*arr按int[4]的方式取地址,得到的是arr[0],也就是第一个数组名,是指针。*(arr +1)表示取下一个“元素”,也就是arr[1],也是指针。

arr[1]=>*(arr+1)    第一行第0列的列地址

&arr[1] => &*(arr+1) =>arr+1 第1行的行地址

*arr+1 =>第0行第1列的列地址
arr[1]+2 =>*(arr+1)+2 =>第1行第2列的列地址
**arr ==*(*(arr+0)+0) == arr[0][0]

*(*(arr+n)+m)== arr[n][m]                                                                                                 在上面过程中我们将arr用作指针,这种指针便是数组指针,本质是指针。

由此我们得出一维数组指针和二维数组的关系

一维数组指针和二维数组名完全等价

有int arr[3][4] == int (*p)[4]

将上述过程扩展,我们可以得到如下结论:

int arr[n] == int *p;

int arr[n][m] == int(*p)[m];

int arr[n][m][k] == int (*p)[m][k];

也就是n维数组和n-1维的数组指针等价(三维可以理解为行,列,页

5.多维数组的物理储存方式

事实上我们知道内存是一片连续的一维空间,在处理二维数组时我们只是人为的把它”折叠”看成了行和列的形式,也就是说多维数组在逻辑上是多维的,在物理上是一维的                                         

所以我们也可以用遍历一维数组的方式来遍历二维数组,

int row = sizeof(arr)/sizeof(arr[0]);

int col = sizeof(arr[0])/sizeof(arr[0][0]);

int *p = &arr[0][0];                                      //指向int,一次取一个int

for(i =0; i<= row*vol; i++){

std::cout<<*(p+i)<<" "                               //也可以写成输出p[i],遍历整个数组

}

 6.字符串与指针

在c和c++中,对于用双引号括起来的字符串如“hello world”,双引号有两个作用,一是表明双引号内的部分是字符串,二是取这个字符串的首地址,对,没错,是首地址,因为字符串==字符数组,也就是说字符串本身就是字符数组                                                                                               对于int *p = "hello world",p储存的是首字母h的地址                                                                         我们来看这样一段代码:

char ani[20] = "bear";

const char *bird = "wren";

char *ps;

ps = "fox";

std::cout<<ani<<endl;                   //输出ani

std::cout<<bird<<endl;                 //输出bird

std::cout<<ps<<"at"<<(int *) ps;   //输出ps和它的地址

会得到如下结果():                                                                                                                                       bear

wren

fox

0x0065fd30                                                                                                                                     一般来说,如果给court提供一个指针,他将打印地址,但如果指针是char*类型,那么court将显示的是指向的字符串,如果要显示字符串的地址,就必须将这种指针强制转换为另一种指纹类型,如int *

7.cv-限定

1.const与指针

const关键字用于指针主要有两种用法,一种是让指针指向一个常量对象,这样可以防止通过指针修改指向的值;一种是将指针声明为常量,这样可以防止指针指向别的位置。

include<iostream>
using namespace std;int main(int n, char *argc)
{
int age = 41;
const int *pt = &age
1.* pt +=1;
2.cin>>*pt;
3.age = 20;  cout<<*pt<<endl;
}

当*pt为const时无法修改*pt,也就无法通过指针修改它指向的值,1,2处都会wrong掉,但age是可改的,因为它没有被const修饰,变量的特性就是可以改值,当age改后 *pt也会跟着修改。             以前我们将常规变量的地址赋给常规指针,而这里将常规变量的地趾赋给指向const的指针。因此还育两种可能:将 const 变量的地址赋给指向 const 的指针、将 const 的地址赋给常规指针。这两种操作都可行吗?第一种可行,但第二种不可行:
const float g = 9.80;
const float *pe = &g;              //对

const float gmoon = 1.63;
float * pm= &g_moon;            //错
对于第一种情况来说,既不能使用g来修改值9.80,也不能使用pe来修改。C++禁止第二种情况的原因很简单——如果将g的地址赋给pm,则可以使用pm来修改g_moon的值,这使得gmoon的const 状态很荒谬,因此C++禁止将const的地址赋给非const指针。如果读者非要这样做,可以使用强制类型转换来达到。

然而,进入两级间接关系时,与一级间接关系一样将const和非const混合的指针赋值方式将不再安全。如果允许这样做,则可以编写这样的代码:

const int **pp2;
int *p1;
const int n = 3;
pp2 = &p1;           // not allowed, but suppose it were
*pp2 = &n;            //valid, both const, but sets p1 to point at n
*pl= 10;                //valid, but changes const n

上述代码将非const地址(&pl)赋给了const指针(pp2),因此可以使用pl来修改const数据。因此,仅当只有一层间接关系(如指针指向基本数据类型)时,才可以将非const地址或指针赋给const指针。

2.cv-限定

2.1.下面就是cv限定符:
const;
volatile。

(读者可能猜到了,cv表示const和volatile)。最常用的cv-限定符是const,而读者已经知道其用途,它表明:内存被初始化后,程序便不能再对它进行修改。
关键字volatile表明,即使程序代码没有对内存单元进行修改,其值也可能发生变化。听起来似乎sb,实际上并非如此。例如,可以将一个指针指向某个硬件位置,其中包含了来自串行端口的时间或信息在这种情况下,硬件(而不是程序)可能修改其中的内容。或者两个程序可能互相影响,共享数据。该键字的作用是为了改善编译器的优化能力。例如,假设编译器发现,程序在几条语句中两次使用了某个?量的值,则编译器可能不是让程序查找这个值两次,而是将这个值缓存到寄存器中。这种优化假设变量值在这两次使用之间不会变化。如果不将变量声明为volatile,则编译器将进行这种优化;将变量声明为volatile,相当于告诉编译器,不要进行这种优化。

2.2. mutable
现在回到mutable。可以用它来指出,即使结构(或类)变量为const,其某个成员也可以被修改。如,请看下面的代码:

struct data
{
char name[30];
mutable int accesses;
...
} ;
const data veep ={"Claybourne Clodde",0,……};
strcpy(veep. name,"Joye Joux");//not allowed
veep. accesses++; //allowed

veep的const限定符禁止程序修改veep的成员,但access成员的mutable说明符使得access不受这种限制。
本文不使用volatile或mutable,但将进一步介绍const。

3.再谈const
在C++(但不是在C语言)中,const限定符对默认存储类型稍有影响。在默认情况下全局变量的链接性为外部的,但const全局变量的链接性为内部的。也就是说,在C++看来,全局const定义(如下述代码段所示) 就像使用了static说明符一样。
const int fingers =10;      //same as static const int fingers =10;
int main(void)
{ ......}
C++修改了常量类型的规则。例如,假设将一组常量放在头文件中,并在同一个程序的多个文件中使用该头文件。那么,预处理器将头文件的内容包含到每个源文件中后,所有的源文件都将包含类似下面这样的定义:
const int fingers =10;
const char *warning ="Wak!";
如果全局const声明的链接性像常规变量那样是外部的,则根据单定义规则,这将出错。也就是说,只能有一个文件可以包含前面的声明,而其他文件必须使用extern关键字来提供引用声明。另外,只有未使用extern关键字的声明才能进行初始化:
//extern would be required if const had external linkage
                                     extern const int fingers; //can't be initialized
                                     extern const char *warning;
因此,需要为某个文件使用一组定义,而其他文件使用另一组声明。然而,由于外部定义的const数据的链接性为内部的,因此可以在所有文件中使用相同的声明。
内部链接性还意味着,每个文件都有自己的一组常量,而不是所有文件共享一组常量。每个定义都是其所属文件私有的,这就是能够将常量定义放在头文件中的原因。这样,只要在两个源代码文件中包括同一个头文件,则它们将获得同一组常量。
如果出于某种原因,程序员希望某个常量的链接性为外部的,则可以使用extern关键字来覆盖默认的内部链接性:
extern const int states =50;//definition with external linkage
在这种情况下,必须在所有使用该常量的文件中使用extern关键字来声明它。这与常规外部变量不同,定义常规外部变量时,不必使用extern关键字,但在使用该变量的其他文件中必须使用extern。然而,请记住,鉴于单个const在多个文件之间共享,因此只有一个文件可对其进行初始化。
在函数或代码块中声明const时,其作用域为代码块,即仅当程序执行该代码块中的代码时,该常量才是可用的。这意味着在函数或代码块中创建常量时,不必担心其名称与其他地方定义的常量发生冲突。

8.对数组名取地址

从前面的讨论我们已经知道数组名仅是相当于指针,数组名本身是一个值为数组首元素地址的常量,在“对数组名取地址”这一表达式中数组名是“数组”这种变量的变量名,&a取的是“数组”这种变量的首地址,就有&a=a=&a[0], &a+1自然也就要跨过整个数组的所有元素长度总和的一个长度。
例如:int a[12],a+1会跨过一个int的长度,而&a+1要跨过12个int的长度,*&a就是数组名,实际上*和&是从左往右成对抵消,因为从左往右取地址时取解引用时类型是一样的,记住这点即可。

四.指针与函数

1. 函数与一维数组

假设我们现在需要设计一个求和函数,用来计算传入函数的int数据之和,那么我们可以用数组来记录数据以省去定义多个变量的麻烦,但是如何来传入数组呢?由于函数计算总数,所以应该返回答案,另外,为了知道传入的是哪个数组,还需要将数组名和数组长度作为参数传递进去,在c/c++中我们有两种做法:

int sum(int arr[],  int n);

int sum(int *arr, int n);

对于前者,[]表示arr是一个数组名,而[]为空表示长度任意;对于后者,由于数组名是指向第一个元素的指针,而每个元素是一个int,所以我们传入的是一个int * 类型的指针,这确保了每次刚好取出一个元素。实际上这同时也意味着int *arr和int arr[]在作为函数参数时是等价的,事实上这两者也只有这种情况下是等价的。

#include <iostream>
const int ArSize =8;
int sumarr(int arr[],int n);
int main()
{
int cookies[ArSize] = {1,2,4,8,16,32,64,128);
std::cout << cookies << " = array address, ";
std::cout << sizeof(cookies)<<" sizeof cookies\n";
int sum = sumarr(cookies, ArSize);
std::cout << "Total cookies eaten:" << sum << std::endl;
sum = sumarr(cookies, 3);
std::cout << "First three eaters ate " << sum << " cookies. \n";
sum = sumarr(cookies + 4, 4);
std::cout << "Last four eaters ate " << sum << " cookies.\n";
return 0;
}int sumarr(int arr[], int n)
{
int total =0;
std::cout<<arr<< "=arr"
std::cout << sizeof(arr)<< "= sizeof arr\n";
for (i = 0;i<n; i++)
total = total + arr[i];
return total;
}

下面是该程序的输出(地址值和数组的长度将随系统而异):
003EF9FC = cookies 地址,                                                                                                          32byte = sizeof cookies

003EF9FC =arr 地址, 4 = sizeof arr    sum: 255

003EF9FC = arr, 4 = sizeof arr  前三个和:7

O03EFAOC = arr, 4 = sizeof arr 后四个 和:240

我们来分析上述结果:                                                                                                                       首先arr是指针,sizeof得到的值在32位平台恒为四字节,这也是必须传入数组长度的原因;其次我们可以传递的不仅是数组本身,也可以是它的子数组,比如我们上面传入arr+4,和4就算出了数组后四个元素之和,另外,如果不想在函数内部修改数组,可以指定参数为const的int *arr。

2.函数与二维数组

一维数组 作为函数的形参会被优化成指针变量,二维数组作为函数的形参会被优化成一维的数组指针。我们仍然有两种表示方法:

int arr[3][4] = {{1,2,3,4},  {9,8,7,6},  {2,4,6,8}};

int sum(int arr[][4]);

int sum(int (*arr)[4]);         //优先级问题,此前已说明过

此时指针类型已经指明了列数,所以不用单独传n,但并没有指明行数,所以仍旧可以传任意行,此时我们可以直接写arr[n][m]来使用元素,也可以用*(*(arr+n)+m),之前已经做过细致的讨论,这里不做赘述。值得一提的是不要让函数返回普通局部变量的地址,比如函数中有普通局部变量data,在将&data返回时函数已经结束,data被释放,这时如果再使用&data很可能会引发段错误。

3.函数指针

3.1 基础函数指针

与数据项类似,函数也有地址,函数的地址是储存其机器语言代码的内存的开始地址,我们可以编写一个将另一个函数的地址作为参数的函数,这样第一个函数能够找到第二个函数并运行它,这与直接调用另一个函数相比,意味着可以在不同时间可以传递不同函数的地址也就是在不同时间使用不同的函数。我们需要通过以下三个步骤来完成上述过程:

1.获取函数地址
这一步最为简单,使用函数名即可,函数名就是函数的入口地址

2.声明函数指针                                                                                                                                   要声明向特定类型的函数的指针,可以首先编写这种函数的原型,然后用 *pf 替换函数名,这样pf就是这类函数的指针,为提供正确的运算符优先级,必须在声明中使用括号将*pf括起,括号的优先级比*高,因此*pf()意味着是一个返回为指针的函数,而(*pf)()意味着pf是一个指向指针的函数:

double f(int);

double *pf(int);           //返回值为指针

double (*pf)(int);         //函数指针

注意,

double ned(double);

int ted(int);

double *pf(int);

pf = ted;         //wrong    返回值不匹配

pf = ned;       //wrong    参数类型不匹配

3.使用指针来调用函数

前面说过,pf这个指针等于函数名,所以直接将pf当成函数名使用即可,然而,(*pf)也可以直接当做函数名使用,也就是double x = pt(5)和double x =(* pt)(5)都是对的,这实际上是历史遗留问题:一种学派认为由于pf是函数指针,而*pf是函数,所以应该将(*pf)用作函数调用,另一种学派认为由于函数名是指向函数的指针,指向函数的指针的行为和函数相似,因此应该将pf()用作函数调用,c++进行了折中,两种方式都是正确的,虽然他们逻辑上是互相冲突的。

不妨来看个示例:

#include <iostream>
double betsy(int);
double pam(int);// second argument is pointer to a type double function that
// takes a type int argument
void estimate(int lines, double (*pf)(int));int main(){
using namespace std;
int code;
cout << "How many lines of code do you need? ";
cin >> code;
cout << "Here's Betsy's estimate:\n";
estimate (code, betsy);
cout << "Here's Pam's estimate:\n";
estimate(code, pam);
return 0;
}double betsy(int lns)
{
return 0.05* lns;
}double pam(int lns)
{
return 0.03 * lns + 0.0004 * lns * 1ns;
}void estimate(int lines,  double (*pf)(int))
{
using namespace std;
cout << lines <<"lines will take";
cout << (*pf)(lines)<<"hour(s)\n";
}

下面是运行该程序的情况:
How many lines of code do you need? 30
Here's Betsy's estimate:
30 lines wiil take 1.5 hour(s)
Here's Pam's estimate:
30 lines will take 1.26 hour(s)
下面是再次运行该程序的情况:
How many lines of code do you need? 100
Here's Betsy's estimate:
100 lines will take 5 hour(s)Here's Pam's estimate:100 lines will take 7.hour(s)

3.2 深入探索函数指针

首先我们先来看三个示例:

const double * f1(const double ar[], int n);

const double * f2(const double  [], int n);

const double * f3(const double *, int n);

这些函数的参数看似不同,但实际上完全相同,首先之前已经说过const double[]的含义和const double *ar完全相同,其次函数原型中可以省略标识符,因此const double ar[]可以转后简化为const double[]而const double *ar也可以简化为const double *,因此上述所有函数特征标的含义都相同,另一方面,函数的定义必须提供标识符,因此需要使用const double *ar或const double ar []

一接下来,假设要声明一个指针,它可指向这三个函数之一。假定该指针名为p1,则只需将目标函数原型中的函数名替换为(*p1):
const double * (*p1)(const double *,int);
可在声明的同时进行初始化:
const double * (*p1)(const double *, int) = f1使用C11的自动类型推断功能时,代码要简单得多:auto p2 = f2; 现在来看下面的语句:
cout << (*p1) (av,3)<<":"<<*(*p1)(av,3)<< endl;
cout << p2(av,3) << ":" << *p2(av,3) << endl;
根据前面介绍的知识可知,(*p1)(av,3)和p2(av,3)都调用指向的函数(这里为f1和f2),并将av和3作为参数。因此,显示的是这两个函数的返回值。返回值的类型为const double*(即double 值的地址),因此在每条cout语句中,前半部分显示的都是一个double 值的地址。为查看存储在这些地址处的实际值,需要将运算符*应用于这些地址,如表达式*(*p1)(av,3)和*p2(av,3)所示。
鉴于需要使用三个函数,如果有一个函数指针数组将很方便。这样将可使用for 循环通过指针依次调用每个函数。如何声明这样的数组呢?显然,这种声明应类似于单个函数指针的声明,但必须在某个地方加上[3],以指出这是一个包含三个函数指针的数组。问题是在什么地方加上[3],答案如下(包含初始化):const double * (*pa[3])(const double *, int) ={fl,f2,f3};

为何将[3]放在这个地方呢?pa是一个包含三个元素的数组,而要声明这样的数组,首先需要使用pa[3]。该声明的其他部分指出了数组包含的元素是什么样的。运算符[]的优先级高于*,因此*pa[3]表明pa是一个包含三个指针的数组。上述声明的其他部分指出了每个指针指向的是什么:特征标为const double*,int,且返回类型为const double*的函数。因此,pa是一个包含三个指针的数组,其中每个指针都指向这样的函数,即将const double*和 int作为参数,并返回一个const double*。

这里能否使用auto呢?不能。自动类型推断只能用于单值初始化,而不能用于初始化列表。但声明数组pa后,声明同样类型的数组就很简单了:
auto pb = pa;
前面说过,数组名是指向第一个元素的指针,因此pa和pb都是指向函数指针的指针。如何使用它们来调用函数呢?pa[i]和pb[i]都表示数组中的指针,因此可将任何一种函数调用表示法用于它们:
const double * px = pa[0](av,3);const double * py =(*pb[1])(av,3);要获得指向的double值,可使用运算符*:double x = *pa[0l(av,3)a
double y = *(*pb[1])(av, 3);
可做的另一件事是创建指向整个数组的指针。由于数组名pa是指向函数指针(数组元素)的指针,因此指向数组的指针将是这样的指针,即它是指向数组指针的指针。这听起来令人恐怖,但由于可使用单个值对其进行初始化,因此可使用auto:
auto pc = &pa; // C11 automatic type deduction
如果您喜欢自己声明,该如何办呢?显然,这种声明应类似于pa的声明,但由于增加了

如果您喜欢自己声明,该如何办呢?显然,这种声明应类似于pa的声明,但由于增加了一层间接,因此需要在某个地方添加一个*。具体地说,如果这个指针名为pd,则需要指出它是一个指针,而不是数组。这意味着声明的核心部分应为(*pd)[3],其中的括号让标识符pd与*先结合:
*pd[3] // an array of 3 pointers
(*pd)[3] // a pointer to an array of 3 elements
换句话说,pd是一个指针,它指向一个包含三个元素的数组。这些元素是什么呢?由 pa的声明的其他部分描述,结果如下
const double *(*(*pd)[3])(const double *, int) = &pa;

这里的*pd得到的是数组的名称,所以*(*pd)[3]是一个指针数组作为函数名(每一个元素都指向函数,都是函数首地址),请注意:const double *是返回值类型,不要在这里犯迷糊

要调用函数,需认识到这样一点:既然pd 指向数组,那么*pd就是数组,而((*pd)[i]是数组中的元素,即函数指针。因此,较简单的函数调用是(*pd)[i](av,3),而*(*pd)[i](av,3)是返回的指针指向的值。也可以使用第二种使用指针调用函数的语法:使用(*(*pd)[i])(av,3)来调用函数,而*(*(*pd)[])(av,3)是指向的double值。请注意pa(它是数组名,表示地址)和&pa之间的差别。正如您在文前面看到的,在大多数情况下pa 都是数组第一个元素的地址,即&pa[0]。因此,它是单个指针的地址。但&pa是整个数组(即三个指针块)的地址。从数字上说,pa和&pa的值相同,但它们的类型不同。一种差别是,pa+1为数组中下一个元素的地址,而&pa+1为数组pa后面一个12字节内存块的地址(这里假定地址为4字节)。另一个差别是,要得到第一个元素的值,只需对pa解除一次引用,但需要对&pa解除两次引用:
**&pa == *pa == pa[0]

4.结构体与指针

4.1 结构体对齐规则

我们先来看结构体变量的储存模式:首先结构体内部的变量并不像数组那样紧凑排列,比如说我们定义一个结构体:

struct Data

{

char c;

int b;

};

因为结构体内的数据类型不同,所以我们推测他可能有如下两种排列方式:

实际上结构体采用的是对齐的排列方式,这是一种典型的以空间换取时间来提高运行效率的方式,诸如此类的还有函数的inline等等,那么如何确认结构体的对齐方式呢?

我们先介绍一个概念--偏移量:偏移量指的是结构体变量中成员的地址和结构体变量首地址的差。即偏移字节数,第一个成员的偏移量为0,然后给出对齐规则:
1、第一个成员在于结构体变量偏移量为0的地址处。                                                                     2、其他成员变量要对齐到对齐数的整数倍的地址处,对齐数=编译器默认的一个对齐数与 该成员大小的较小值。
3、结构体总大小为最大对齐数(每个成员变量都有自己的对齐数)的整数倍。
4、针对嵌套结构体,嵌套的结构体要对齐到自己最大对齐数的整数倍处,结构体总体大小是所有对齐数的最大值(包含套结构体的对齐数)的整数倍。

我们可以用处理二维数组的方法(逻辑上是多行,物理上是一行)将上述过程简化为以下三个步骤:

1、确定分配单位(一行分配多少字节)                                                                                           由结构体中最大的基本类型长度决定。                                                                                            2、确定成员的偏移量
成员偏移量=min(成员自身类型大小,编译器默认的一个对齐数)的整数倍。
3、收尾工作
结构体的总大小=分配单位整数倍

vs的对齐数默认为8,我们来看几个案例:

对于·char c和int b,int最大,每行分配4个单位,从上往下依次填入元素,对a,0 = 2*0,所以放置第0位,对b,4 = 4*1,所以放在第四位,一共用两行即可,故sizeof为8.

对下面这个也是一样:

struct Data

{

char a;

short b;

int c;

char d;

short e;

};

当然我们也可以自行修改对齐数,只需要在结构体前写下#pragma pack(对齐数),在你希望结束这种修改的位置写入#pragma pack()即可(需包含stddef.h头文件)。

4.2 结构体指针变量和含指针结构体的深拷贝问题

明白了结构体的对齐规则,显然现在再使用*(p+1)这样的语句是非常危险的,因为确认内部的对齐方式有时候很耗时间,所幸我们有更好的方法来达到访问成员的目的:  ->    ,如果是地址可以直接使用->访问成员,如果是结构体变量需要是 . 访问成员,  . 的优先级是最高的:

 #include<string.h>
struct Stu{
int num;
char name [32];
int *p;
}
int main(){
Stu lucy={100,"lucy",NULL}
Stu *p=&lucy;
cout<<lucy.num<<" "<<lucy.name<<endl;
cout<<(*p).num<<""<<(*p).name<<endl;
//通过指针变量使用->访问成员
cout<<p->num<<""<<p->name<<endl;
//如果是地址可以直接使用->访问成员 如果是结构体变量 需要是.访问成员
cout<<(&lucy)->num<<" "<<(&lucy)->name<<endl;}

输出为:100 lucy   100 lucy    100 lucy    100 lucy

同时结构体之间的赋值是普通的复制,也就是说如果我们编写如下的程序:

#include<string.h>
struct Stu

{
int num;
char *name;
};
void use(){
Stu lucy;
lucy.num = 100;
lucy.name = new char[32];
strcpy(lucy.name,"hello world");

Stu bob;
bob = lucy;
delete []lucy.name;
delete []bob.name;
}

此时的 = 代表浅拷贝,也就是原封不动的将Lucy的内容给了Bob,但“hello world”仍然只有一份:

当我们直接释放结构的空间时只会删除name而不会影响“hello world”,所以要进行额外的删除,即delete []lucy.name;所以同时浅拷贝很容易造成重复释放同一内存导致错误,所以在结构体有指针成员时应进行深拷贝,如:

Stu bob;
bob.num = lucy.num;

bob.name = new char[32];

strcpy(bob.name,lucy.name);

五.this指针与虚函数原理(c++)

1.this指针

类的与结构一样的一点是两者在定义的时候均不占内存,只有将类和结构实例化为对象的时候才会占用内存(值得一提是的c++做了结构体强化,使得结构体内部也可以定义函数,如此结构体与单类仅有成员访问权限一个区别,即结构体的成员均为public),在用同一个类创建多个对象的时候,普通成员变量也会被创建多个并分配给每一个对象,但普通成员函数只会有一个,也就是说多个对象其实调用的是同一个函数:

(提一嘴,静态成员函数只能访问静态成员变量,甚至无法使用this指针)

那么如何区分是哪个类调用的这个函数呢?这就是this解决的问题之一了:

1.2.调用与同名问题                                                                                                                        this指针指向用来调用成员函数的对象,每一个成员函数都有一个this指针,指向调用它的类成员,*this也因此可以当做对象的别名来使用(意味着它是可改的)成员函数通过this指针即可知道操作的是那个对象的数据。this指针是一种隐含指针,它隐含于每个类的非静态成员函数中,也就是说上图中的this->ma = 3和ma = 3其实都是对的,因为this->编译器会替你加上,即this指针无
需定义,直接使用即可,另外如果您只想修改类中的一部分变量并且类中某些变量与函数参数同名时,可以这样:

Data class
{

public:
    int a;
    int b;
    mutable int c;public:
    Data(int a, int b,int c){
    this->a = a;

this->b = b;

this->c = C;

}
//const 修饰成员函数为只读(该成员函数不允许对 成员数据 赋值)
void showData(void) const

{
//a = 100;//wrong
C=100;   //right
cout<<a<<""<<b<<" "<<c<<endl;
//mutable修饰的成员除外

}
};
int main()
{
Data ob1(10,20,30);
ob1.showData();

}

如此使用const后将只有被mutable修饰的变量可以更改自身的值,而   this->a = a;也解决了同名问题。

注意:静态成员函数内部没有this指针,静态成员函数不能操作非静态成员变量

3、this来完成链式操作

using namespace std;
class Data{
public:
Data& myPrintf(char *str)
{
cout<<str<<"
return *this;//返回调用该成员函数的对象}
};
void testo1()
{
Data().myPrintf("hehe").myPrintf("thth").myPrintf("wuwu");
}

我们用第一个类调用了函数之后返回了this指针取地址的结果的别名,也就是原本的类,此时仍然可以继续调用,由此达到一种循环调用的结果,所以输出为:hehe thth wuwu.

2.effective c++

补充一点,在c++的构造函数中,更常用的是成员列表的形式,这样更加高效。

成员初始化列表的语法:

如果Classy是一个类,而meml、mem2和mem3都是这个类的数据成员,则类构造函数可以使用如下的语法来初始化数据成员:
Classy::Classy( int n,int m) :meml(n),mem2(0),mem3(n*m +2)
{
//...
}
上述代码将meml初始化为n,将mem2初始化为0,将mem3初始化为 ⁤n*m+2。⁤ 从概念上说,这些初始化工作是在对象创建时完成的,此时还未执行括号中的任何代码。请注意以下几点:
这种格式只能用于构造函数;
必须用这种格式来初始化非静态const数据成员(至少在C++11之前是这样的);
必须用这种格式来初始化引用数据成员。
数据成员被初始化的顺序与它们出现在类声明中的顺序相同,与初始化器中的排列顺序无关。
警告:不能将成员初始化列表语法用于构造函数之外的其他类方法。
成员初始化列表使用的括号方式也可用于常规初始化。也就是说,如果愿意,可以将下述代码:
int games =162;
double talk =2.71828;
替换为:
int games(162);
double talk(2.71828);
这使得初始化内置类型就像初始化类对象一样。

对于继承类,新版本的构造函数将使用成员初始化列表语法,它使用类名而不是成员名来标识构造函数:
Student(const char *str,const double *pd,int n):std::string(str),ArrayDb(pd,n){}//use class names for inheritance

初始化顺序:
当初始化列表包含多个项目时,这些项目被初始化的顺序为它们被声明的顺序,而不是它们在初始化列表中的顺序。例如,假设Student构造函数如下:
Student(const char *str,const double *pd,int n):scores(pd,n),name(str) {}
则name成员仍将首先被初始化,因为在类定义中它首先被声明。对于这个例子来说,初始化顺序并不重要,但如果代码使用一个成员的值作为另一个成员的初始化表达式的一部分时,初始化顺序就非常重要了。

2.虚函数原理简介

如果我们想定义一个函数,让他可以操作同一个父类派生出的任何子类,我们可以这样做:

函数重点需要关注的部分是返回值类型,参数和函数名,显然我们可以从参数下手,那么我们要操作所有子类就需要找到所有子类的共性--都来自同一个父类,所以我们可以将行参设置为父类的指针,由于是父类的指针,所以->只能取出来父类的部分无法直接调用子类的部分(子类继承了父类),那么如何通过父类的指针去访问子类呢?就是虚函数,这里给出一个案例,只讲虚函数的内部逻辑:

#include <iostream>
using namespace std;
class Animal
{
public://虚函数virtual void speak(void){cout<<"动物在说话"<<endl;}
};
class Dog:public Animal
{
public:
#if 1
//子类重写 父类的虚函数void speak(void){cout<<"狗在汪江"<<end1;
}
#endif};
class Cat:public Animal
{public:
//子类重写 父类的虚函数
void speak(void){
cout<<"猫在喵喵"<<endl;
}};void Animalspeak(Animal *p)
{
p->speak();
return;
}int main(int argc, char *argv[])
{
AnimalSpeak(new Dog);//狗在汪汪
Animalspeak(new Cat);//猫在嘴喵
return 0;
}

如果父类中的成员函数被virtual修饰,那么就会产生一个虚基指针指向一张虚函数表,如果不涉及到继承,这张表保存的就是这个成员函数自己的地址,然而当涉及到继承的时候,子类会将父类的虚基指针和虚函数表一并继承,同时会将虚函数表中父类成员函数的地址更改为自己与父类那个成员函数同名的成员函数(如果有的话)的的地址,这时使用父类的virtual成员函数实际上调用的是子类的同名成员函数,因为调用过程中通过虚函数表找地址的时候找到的是被继承的子类的同名函数的地址,这样就达到了用父类指针调用子类成员的目的

虚析构也是如此,调用父类的析构函数时会根据虚函数表转而去调用子类的析构函数,而子类在析构中又会将其他类成员和父类一同析构,从而达到三者均析构的效果

part2. 深入部分

(建议有较高的c++水平,最低要求是熟读过不下于600页的经典书籍和30000的码量)

六.语意学

1. 再谈new和delete

运算符new的使用,看起来似乎是个单一运算,像这样:
     int *pi = new int( 5 );
     但事实上它是由两个步骤完成的:
1.通过适当的new运算符函数实例,配置所需的内存:
  //调用函数库中的new运算符 
⁤⁤  int‘pi=new(sizeof(int));⁤⁤
2.将配置得来的对象设立初值: 
⁤⁤  ∗pi=5;⁤⁤
  更进一步地说,初始化操作应该在内存配置成功(经由new运算符)后才执行:
  //new运算符的两个分离步骤
  //given:  int *pi =new int( 5);
  //重写声明
  int *pi;
  if( pi= new( sizeof( int)))
  *pi =5;
  delete运算符的情况类似。当程序员写下:
  delete pi;
时,如果pi的值是0,C++语言会要求delete 运算符不要有操作。因此编译器必须为此调用构造一层保护膜:
  if ( pi != 0 )
   delete( pi);
  请注意pi并不会因此被自动清除为0,因此像这样的后继行为: 
  if ( pi && *pi ==5 ) ...
       虽然没有良好的定义,但是可能(也可能不)被评估为真。这是因为对于pi所指向之内存的变更         或再使用,可能(也可能不)会发生,也是因此在数据结构中经常看到delete和重新设置nullptr的操作
 pi所指对象的生命会因delete 而结束。然而,把pi继续当做一个指针来用,仍然是可以的(虽然其使用受到限制),例如:
  pi仍然指向合法空间
  甚至即使存储于其中的object 已经不再合法
  if (pi ==sentinel)…
  在这里,使用指针pi,和使用pi所指的对象,其差别在于哪一个的生命已经结束了。虽然该地址上的对象不再合法,地址本身却仍然代表一个合法的程序空间。因此pi能够继续被使用,但只能在受限制的情况下,很像一个void*指针的情况。
  以构造函数来配置一个class object,情况类似。例如:
  Point3d  *origin =new Point3d;
被转换为:
  

Point3d  *origin;
  //C++伪码
  if( origin = new( sizeof( Point3d)))
  origin =Point3d::Point3d(origin );
  如果实现出exception handling,那么转换结果可能会更复杂些:
  //C++伪码
  if( origin= new( sizeof( Point3d))){

try {
  origin =Point3d::Point3d(origin );
  }
  catch(……){

//调用delete library function以释放因new 而配置的内存
        delete( origin);
  //将原来的exception上传
  throw;
         }
  }

在这里,如果以new运算符配置object,而其constructor抛出一个exception,配置得来的内存就会被释放掉。然后exception再被抛出去(上传)。
  Destructor的应用极为类似。下面的式子:
  delete origin;
会变成:
  if ( origin !=0) {
  //C++伪码
  Point3d::~Point3d( origin);
   delete( origin);
  }
  如果在exceptionhandling的情况下,destructor应该被放在一个try区段中。exception handler会调用delete运算符,然后再一次抛出该exception。
  一般的library对于new运算符的实现操作都很直截了当,但有两个精巧之处值得斟酌(请注意,以下版本并未考虑exception handling):
  

extern void*
  operator new( size_t size)
  {
  if ( size == 0 )
  size = 1 ;
  void* last_alloc;
  while(!( last_alloc= malloc( size)))
  {
  if( newhandler)(*_new_handler)( );
  else
  return 0;
  }
  return last alloc;
  }

  虽然这样写是合法的:  
  new T [ 0 ] ;
但语言要求每一次对new的调用都必须传回一个独一无二的指针。解决此问题的传统方法是传回一个指针,指向一个默认为1-byte的内存区块(这就是为什么程序代码中的size被设为1的原因)。这个实现技术的另一个有趣之处是,它允许使用者提供一个属于自己的_new_handler( )函数。这正是为什么每一次循环都调用 new handler( )之故。
  new运算符实际上总是以标准的C malloc()完成,虽然并没有规定一定得这么做不可。相同情况,delete运算符也总是以标准的C free()完成:
  extern void
  operator delete(void *ptr)
  {
  if(ptr)
  free( (char*)ptr );
  }

针对数组的new语意 
  当我们这么写:
 
⁤⁤  int* parray=new int[5];⁤⁤
 
⁤⁤  vec_new()⁤⁤不会真正被调用,因为它的主要功能是把默认构造函数施行于
  class objects所组成的数组的每一个元素身上。倒是new运算符函数会被调用:
       int *p_⁤⁤ray=(int*)_new(5*sizeof(int));⁤⁤
        相同情况,如果我们写: 
  // struct simple_aggr{ float f1, f2;};
  simple_aggr  *p_aggr  =  new simple_aggr[5];
vec_new( )也不会被调用。为什么? simple_aggr 并没有定义一个构造函数或析构函数,所以配置数组以及清除p_aggr 数组的操作,只是单纯地获得内存和释放内存而已。这些操作由new和delete运算符来完成就绰绰有余了。
  然而如果 class 定义了一个 default constructor,某些版本的 vec_new( )就会被调用,配置并构造class objects所组成的数组。例如这个算式:
  Point3d  *p_array  = new Point3d[10];
通常会被编译为:
  Point3d*  p_array;
  p_array= vec_new(0, sizeof( Point3d),10,
&Point3d::Point3d,
&Point3d::~Point3d );

在个别的数组元素构造过程中,如果发生exception,构造函数就会被传递给 vec_new( )。只有已经构造妥当的元素才需要构造函数的施行,因为它们的内存已经被配置出来了, vec_new( )有责任在 exception 发生的时机把那些内存释放掉。
  在C++2.0版之前,将数组的真正大小提供给程序的delete运算符,是程序员的责任。因此如果我们原先写下:
  int array_size=10;
  Point3d*  P_array= new Point3d[ array_size];
那么我们就必须对应地写下:
  delete[ array_size]  p_array;

  在2.1版中,这个语言有了一些修改,程序员不再需要在delete时指定数组元素的个数,因此我们现在可以这样写:
  delete[] p_array;
  然而为了回溯兼容,两种形式都可以接受。这项技术支持需要知道的首先是指针所指的内存空间,然后是其中的元素个数。
  寻找数组维度,对于delete运算符的效率带来极大的冲击,所以才导致这样的妥协:只有在中括号出现时,编译器才寻找数组的维度,否则它便假设只有单独一个objects要被删除。如果程序员没有提供必须的中括号,像这样:
  delete p array;
那么就只有第一个元素会被析构。其他的元素仍然存在——虽然其相关的内存已经被要求归还了。

应该如何记录元素个数? 一个明显的方法就是为 vec_new( )所传回的每一个内存区块配置一个额外的word,然后把元素个数包藏在那个word之中。通常这种被包藏的数值称为所谓的cookie。然而,Jonathan和Sun编译器决定维护一个“联合数组(associative array)”,放置指针及大小, 也把destructor的地址维护于此数组之中。
  cookie策略有一个引起忧虑的话题就是,如果一个坏指针应该被交给delete_vec( ),取出来的 cookie 自然是不合法的。一个不合法的元素个数和一个坏的起始地址,会导致destructor以非预期的次数被施行于一段非预期的区域。然而在“联合数组”的政策之下,坏指针的可能结果就只是取出错误的元素个数而已。
 在原始编译器中,有两个主要函数用来存储和取出所谓的cookie:
  // array key 是新数组的地址
  //mustn't either be 0 or already entered
  // elem_count is the count; it may be 0
  typedef void *PV;
  extern int insert new array(PV array key, int elem count);
  //从表格中取出(并去除) array_key
       //若不是传回 elem_count,就是传回 -1
extern int removeoldarray(PV arraykey);

下面是 cfront 中的 vec_new( )原始内容经过修润后的一份呈现,并附加注释:

PV vecnew(PV ptrarray, int elemcount,int size, PV construct)
{//如果 ptr_array 是0, 从 heap 之中配置数组。//如果 ptr_array 不是0,表示程序员写的是://T array[count]//或// new( ptr_array) T[10];int alloc=0;    //我们要在 vec_new 中配置吗?int array_sz= elem_count* size;if( alloc= ptr_array ==0)//全局运算符new ...ptr_array=PV( new char[ array_sz]);//在exception handling之下,//将抛出 exception bad_allocif( ptr_array==0)return 0;//把数组元素个数放到cache中int status = insert newarray( ptrarray, elemcount);if ( status ==-1) {//在exception handling之下将抛出exception//将抛出 exception bad_allocif(alloc)delete ptr_array;return 0;}if (construct){register char* elem=( char*) ptr_array;register char* lim= elem + array_sz;//PF是一个typedef,代表一个函数指针register PF fp =PF(constructor);while (elem <lim){//通过fp调用constructor作用于//'this'元素上(由elem指出)(*fp)( (void*)elem);//前进到下一个元素elem +=size;}}return PV( ptr array);}

vec_delete( )的操作差不多,但其行为并不总是C++程序员所预期或需求的。例如,已知下面两个class声明:
  class Point {
  public:
      Point ( ) ;
      virtual ~Point();
  // ...
  } ;
  class Point3d:public Point {
  public:
      Point3d( ) ;
      virtual ~Point3d();
  / / ...
  } ;

如果我们配置一个数组,内含10个Point3d objects, 我们会预期Point和Point3d的constructor被调用各10次,每次作用于数组中的一个元素:
  //完全不是个好主意
  Point  *ptr =new Point3d[10];
  而当我们delete“由ptr所指向的10个Point3d元素”时,会发生什么事情?很明显,我们需要虚拟机制的帮助,以获得预期的Point destructor和Point3d  destructor各10次的调用(每一次作用于数组中的一个元素):
  //喔欧:这并不是我们所要的
  //只有Point::~Point被调用⋯⋯
  delete []ptr;
  施行于数组上的 destructor,如我们所见,是根据交给 vec_delete( )函数的“被删除之指针类型的destructor”——本例中正是Point destructor。这很明显并非我们所希望。此外,每一个元素的大小也一并被传递过去。这就是 vec delete( )如何迭代走过每一个数组元素的方式。本例中被传递过去的是Point class object的大小而不是Point3d class object的大小。整个运作过程非常不幸地失败了,不只是因为执行了错误的destructor,而且自从第一个元素之后,该destructor即被施行于不正确的内存区块中。

程序员应该怎么做才好?最好就是避免以一个base class指针指向一个derived class objects所组成的数组——如果derived class object比其base大的话(通常如此)。如果你真的一定得这样写程序,解决之道在于程序员层面,而非语言层面:
for( int ix=0; ix< elem_count; ++ ix)
  {
  Point3d *p=&((Point3d*)ptr)[ix];
  }
  基本上,程序员必须迭代走过整个数组,把delete运算符实施于每一个元素身上。以此方式,调用操作将是virtual,因此,Point3d和Point的destructor都会施行于数组中的每一个objects身上。

Placement Operator new的语意

  前面提到过,有一个预先定义好的重载的(overloaded)new运算符,称为placement operator new。它需要第二个参数,类型为void*。调用方式如下:
  Point2w  *ptw =new(arena) Point2w;
  其中arena指向内存中的一个区块,用以放置新产生出来的Point2w object。它只要将“获得的指针(上例的arena)所指的地址传回即可:

void* operator new( size_t, void*p)
  {
      return p;
  }
  如果它的作用只是传回其第二个参数,那么它有什么价值呢? 也就是说,为什么不简单地这么写算了:
  Point2w *ptw =(Point2w*) arena;
  事实上这只是所发生的操作的一半而已。另外一半无法由程序员产生出来。

想想这些问题:
1.什么是使placement new operator能够有效运行的另一半扩充(而且是“arena的显式指定操作(explicit assignment)”所没有提供的)?
2.什么是arena指针的真正类型?该类型暗示了什么?
  Placement new operator所扩充的另一半是将Point2w constructor自动实施于arena所指的地址上:
  //C++伪码
  Point2w *ptw =(Point2w*) arena;
  if ( ptw != 0 )
  ptw->Point2w::Point2w();

这正是使placement operator new威力如此强大的原因。这一份代码决定objects被放置在哪里;        编译系统保证object的constructor会施行于其上。
 然而却有一个轻微的不良行为。你看得出来吗?下面是一个有问题的程序片段:
  //让arena成为全局性定义
  void fooBar(){
  Point2w *p2w =new(arena) Point2w;
  // ... do it ...
  //... now manipulate a new object...
  p2w =new(arena) Point2w;
  }

如果placement operator在原已存在的一个object上构造新的object,而该既存的object有个destructor,这个destructor并不会被调用。调用该destructor的方法之一是将那个指针delete掉。不过在此例中如果你像下面这样做,绝对是个错误:
  //以下并不是实施destructor的正确方法
  delete p2w;
  p2w =new(arena) Point2w;
  是的,delete 运算符会发生作用,这的确是我们所期待的。但是它也会释放由p2w所指的内存,这却不是我们所希望的,因为下一个指令就要用到p2w了。因此,我们应该显式地调用destructor并保留存储空间以便再使用1:
  //施行destructor的正确方法p2w->~Point2w;
  p2w =new(arena) Point2w;
  剩下的唯一问题是一个设计上的问题:在我们的例子中对placement operator的第一次调用,会将新object构造于原已存在的object之上吗?还是会构造于全新地址上?也就是说,如果我们这样写:
  Point2w *p2w =new(arena) Point2w;
我们如何知道arena所指的这块区域是否需要先析构?这个问题在语言层面上并没有解答。一个合理的习俗是令执行new的这一端也要负起执行destructor的责任。
  另一个问题关系到arena所表现的真正指针类型。C++ Standard说它必须指向相同类型的class,要不就是一块“新鲜”内存,足够容纳该类型的object。注意,
 derived class 很明显并不在被支持之列。对于一个derived class,或是其他没有关联的类型,其行为虽然并非不合法,却也未经定义。
  “新鲜”的存储空间可以这样配置而来:

char *arena =new char[sizeof(Point2w)];
  相同类型的object则可以这样获得:
  Point2w *arena = new Point2w;
  不论哪一种情况,新的Point2w的存储空间的确是覆盖了arena的位置,而此行为已在良好控制之下。然而,一般而言,placement new operator并不支持多态(polymorphism)。被交给new的指针,应该适当地指向一块预先配置好的内存。如果derived class比其base class大,例如:
  Point2w *p2w =new(arena)Point3w;
       Point3w的constructor将会导致严重的破坏。
  Placement new operator被引入C++2.0时,最晦涩隐暗的问题就是下面这个由Jonathan Shopiro提出的问题:
  struct Base {int j;virtual void f();};
  struct Derived:Base {void f();};
  void fooBar(){
  Base b;
  b.f () ; //Base::f()被调用
  b.~Base();
  new ( &b) Derived;//1
  b.f ( ) ; //哪一个f()被调用?
  }

由于上述两个classes有相同的大小,把derived object放在为base class而配置的内存中是安全的。然而,欲支持这一点,或许必须放弃对于“经由objects静态调用所有virtual functions(像是b. f())”通常都会有的优化处理。结果,placement
new operator的这种使用方式在Standard C++中未能获得支持。于是上述程序的行为没有明确定义:我们不能够斩钉截铁地说哪一个f函数实例会被调用。尽管大部分使用者可能以为调用的是Derived::f(),但大部分编译器调用的却是Base::f()。

2.fuction语意学

1.1 虚拟成员函数

我们已经看过了virtual function的一般实现模型:每一个class有一个virtualtable,内含该class之中有作用的virtual function的地址, 然后每个object有一个vptr,指向virtual table的所在。在这一部分,我们要走访一组可能的设计,然后以单一继承、多重继承和虚拟继承等各种情况,从细部上探究这个模型。
  

为了支持virtual function机制,必须首先能够对于多态对象有某种形式的“执行期类型判断法(runtime type resolution)”。也就是说以下的调用操作将需要ptr在执行期的某些相关信息,
  ptr->z ( ) ;
如此一来才能够找到并调用z()的适当实例。
  或许最直截了当但是成本最高的解决方法就是把必要的信息加在ptr身上。在这样的策略之下,一个指针(或是一个reference)持有两项信息:
1.它所参考到的对象的地址(也就是目前它所持有的东西);
2.对象类型的某种编码,或是某个结构(内含某些信息,用以正确决议出z()函数实例)的地址。
  这个方法带来两个问题:第一,它明显增加了空间负担,即使程序并不使用多态(polymorphism);第二,它打断了与C程序间的链接兼容性。
  如果这份额外信息不能够和指针放在一起,下一个可以考虑的地方就是把它放在对象本身。但是哪一个对象真正需要这些信息呢?我们应该把这些信息放进可能被继承的每一个集合体身上吗?请考虑一下这样的C struct声明:
  struct date {int m, d, y;};
  严格地说,这符合上述规范。然而事实上它并不需要那些信息。加上那些信息将使C struct膨胀并且打破链接兼容性,却没有带来任何明显的补偿利益。
  “好吧,”你说,“只有面对那些显式使用了class关键词的声明,才应该加上额外的执行期信息。”这么做就可以保留语言的兼容性了,不过仍然不是一个够聪明的政策。举个例子,下面的class符合新规范:
  class date {public:int m,d,y;};
但实际上它并不需要那份信息。下面的class声明虽然不符合新规范,却需要那份信息:
  struct geom {public:virtual ~geom();⋯};
  是的,我们需要一个更好的规范,一个“以class 的使用为基础,而不在乎关键词是class或struct的规范。如果class真正需要那份信息,它就会存在;如果不需要,它就不存在。那么,到底何时才需要这份信息?很明显是在必须支持某种形式之“执行期多态(runtime polymorphism)”的时候。
  在C++中,多态(polymorphism)表示“以一个public base class的指针(或reference),寻址出一个derived class object”的意思。例如下面的声明:
  Point *ptr;
我们可以指定ptr以寻址出一个Point2d对象:

ptr =new Point2d;
  ptr =new Point3d;

ptr的多态机能主要扮演一个输送机制(transport mechanism)的角色,经由它,我们可以在程序的任何地方采用一组public derived类型。这种多态形式被称为是消极的(passive), 可以在编译时期完成——virtual base class的情况除外。
  当被指出的对象真正被使用时,多态也就变成积极的(active)了。下面对于virtual function的调用,就是一例:
  // “积极多态(active polymorphism)”的常见例子
  ptr->z( ) ;
  在runtime type identification(RTTI)性质于1993年被引入C++语言之前,C++对“积极多态(active polymorphism)”的唯一支持,就是对于virtual function call的决议(resolution)操作。有了RTTI,就能够在执行期查询一个多态的pointer或多态的reference了。
  // “积极多态(active polymorphism)”的第二个例子
  if ( Point3d *p3d =
  dynamic_cast< Point3d*.>( ptr))
  return p3d->_z;

所以,问题已经被区分出来,那就是:欲鉴定哪些classes展现多态特性,我们需要额外的执行期信息。一如我所说,关键词class 和struct并不能够帮助我们。由于没有导入像是polymorphic之类的新关键词,因此识别一个class是否支持多态,唯一适当的方法就是看看它是否有任何virtual function。只要class拥有一个virtual function,它就需要这份额外的执行期信息。
  下一个明显的问题是,什么样的额外信息是我们需要存储起来的?也就是说,如果我有这样的调用:
  ptr->z( ) ;
其中z()是一个virtual function,那么什么信息才能让我们在执行期调用正确的z()实例?我需要知道:
1. ptr所指对象的真实类型。这可使我们选择正确的z()实例;
2. z()实例的位置,以便我能够调用它。
  在实现上,首先我可以在每一个多态的class object身上增加两个members:
1.一个字符串或数字,表示class的类型;
2.一个指针,指向某表格,表格中持有程序的virtual functions的执行期地址。

表格中的virtual functions地址如何被建构起来?在C++中,virtual functions(可经由其class object 被调用)可以在编译时期获知。此外,这一组地址是固定不变的,执行期不可能新增或替换之。由于程序执行时,表格的大小和内容都不会改变,所以其建构和存取皆可以由编译器完全掌控,不需要执行期的任何介入。
  然而,执行期备妥那些函数地址,只是解答的一半而已。另一半解答是找到那些地址。两个步骤可以完成这项任务:
1.为了找到表格,每一个class object被安插了一个由编译器内部产生的指针,指向该表格。
2.为了找到函数地址,每一个virtual function被指派一个表格索引值。
  这些工作都由编译器完成。执行期要做的,只是在特定的virtual table slot(记录着virtual function的地址)中激活virtual function。
  一个class只会有一个virtual table。每一个table内含其对应之class object中所有active virtual functions函数实例的地址。这些active virtual functions包括:
1. 这一class 所定义的函数实例。它会改写一个可能存在的base class virtual function函数实例。

2. 继承自base class的函数实例。这是在derived class决定不改写virtualfunction时才会出现的情况。
3. 一个 pure_virtual_called( )函数实例,它既可以扮演 pure virtual function的空间保卫者角色,也可以当做执行期异常处理函数(有时候会用到)。
  每一个virtual function都被指派一个固定的索引值,这个索引在整个继承体系中保持与特定的virtual function的关系。例如在我们的Point class体系中:

class Point {
  public:
  virtual ~Point();
  virtual Point&mult(float)=0;
  //...其他操作
  float x( ) const{ return x;}
  virtual float y()const {return 0;}
  virtual float z()const {return 0;}
  / / ...
  protected:
  Point( float x =0.0);
  float x;
  } ;

virtual destructor被指派slot 1,而mult()被指派slot 2。此例并没有mult()的函数定义,因为它是一个 pure virtual function。所以 pure virtualcalled( )的函数地址会被放在slot 2中。如果该函数意外地被调用,通常的操作是结束掉这个程序。y()被指派slot 3而z()被指派slot 4。x()的slot是多少?答案是没有,因为x()并非virtual function。下图表示Point的内存布局和其virtual table。

当一个class派生自Point时,会发生什么事?例如class Point2d:
class Point2d:public Point {
public:
  Point2d( float x =0.0,float y =0.0)
  : Point(x), y(y){}
  ~Point2d();
  //改写base class virtual functions
  Point2d&mult(float );
  float y( ) const{ return_y;}
    //……其他操作
       protected:
  float y;
  } ;
  一共有三种可能性:
1.它可以继承base class所声明的virtual functions的函数实例。正确地说是,该函数实例的地址会被拷贝到derived class的virtual table的相对应slot之中。
2.它可以使用自己的函数实例。这表示它自己的函数实例地址必须放在对应的slot之中。
3.它可以加入一个新的virtual function。这时候virtual table的尺寸会增大一个slot,而新的函数实例地址会被放进该slot之中。
  Point2d的virtual table在slot 1中指出destructor,而在slot 2中指出mult()(取代pure virtual function)。它自己的y()函数实例地址放在slot 3中,继承自Point的z()函数实例地址则放在slot 4中。

类似的情况,Point3d派生自Point2d,如下:
  class Point3d:public Point2d{
  public:
  Point3d( float x =0.0,float y =0.0,float z =0.0)
  : Point2d(x,y), z(z){}
  ~Point3d();
  //改写base class virtual functions
  Point3d&mult(float);
  float z( ) const{ return z;}
  //...其他操作
  protected:
  float z;
  } ;
  其virtual table中的slot 1放置Point3d的destructor, slot 2放置Point3d::mult()函数地址,slot 3放置继承自Point2d的y()函数地址,slot 4放置自己的z()函数地址。
  现在,如果我有这样的式子:
  ptr->z( ) ;
  我如何有足够的知识在编译时期设定virtual function的调用呢?
  一般而言,在每次调用z()时,我并不知道ptr所指对象的真正类型。然而我知道,经由ptr可以存取到该对象的virtual table。
  虽然我不知道哪一个z()函数实例会被调用,但我知道每一个z()函数地址都被放在slot 4中。
  这些信息使得编译器可以将该调用转化为:

*ptr->vptr[ 4] ) ( ptr );
  在这一转化中,vptr表示编译器所安插的指针,指向virtual table;4表示z()被指派的slot编号(关系到Point体系的virtual table)。唯一一个在执行期才能知道的东西是:slot 4所指的到底是哪一个z()函数实例?
  在一个单一继承体系中,virtual function机制的行为十分良好,不但有效率而且很容易塑造出模型来。但是在多重继承和虚拟继承之中,对virtual functions的支持就没有那么美好了。

2. 多重继承下的Virtual Functions

在多重继承中支持virtual functions,其复杂度围绕在第二个及后继的baseclasses身上,以及“必须在执行期调整this指针”这一点。以下面的class体系为例:

//class体系,用来描述多重继承(MI)情况下支持virtual function 时的复杂度class Basel {public:Basel () ;virtual ~Basel();virtual void speakClearly();virtual Basel *clone()const;
protected:float data_Basel;
} ;class Base2 {
public:Base2 ( ) ;virtual ~Base2();virtual void mumble();virtual Base2 *clone()const;protected:float data_Base2;
} ;
class Derived:public Basel,public Base2 {
public:Derived( ) ;virtual ~Derived();virtual Derived *clone()const;
protected:float data_Derived;
} ;

“Derived支持virtual functions”的困难度,统统落在Base2 subobject身上。

有三个问题需要解决,以此例而言分别是:

(1)virtual destructor,

(2)被继承下来的Base2::mumble(),

(3)一组clone()函数实例。

让我依序解决每一个问题。
  首先,我把一个从heap中配置而得的Derived对象的地址,指定给一个Base2指针:
  Base2  *pbase2 =new Derived;
  新的Derived对象的地址必须调整以指向其Base2 subobject。编译时期会产生以下的代码:
  //转移以支持第二个base class
  Derived *temp =new Derived;
  Base2 *pbase2 = temp ?temp + sizeof( Base1 ) :0;
  如果没有这样的调整,指针的任何“非多态运用” (像下面那样)都将失败:
  //即使pbase2被指定一个Derived对象,这也应该没有问题
  pbase2-> data Base2;
  当程序员要删除pbase2所指的对象时:
  //必须首先调用正确的virtual destructor函数实例
  //然后施行delete 运算符。
  //pbase2可能需要调整,以指出完整对象的起始点
  delete pbase2;
指针必须被再一次调整,以求再一次指向Derived对象的起始处(推测它还指向Derived对象)。然而上述的offset加法却不能够在编译时期直接设定,因为pbase2 所指的真正对象只有在执行期才能确定。
  一般规则是,经由指向“第二或后继之base class”的指针(或reference)来调用derived class virtual function。译注:就像本例的:
  Base2 *pbase2 =new Derived;
  ...
  delete pbase2;//invoke derived class's destructor (virtual)
其所连带的必要的“this指针调整”操作,必须在执行期完成。也就是说,offset的大小,以及把offset加到this指针上头的那一小段程序代码,必须由编译器在某个地方插入。问题是,在哪个地方?
  比较有效率的解决方法是利用所谓的thunk:

Thunk技术初次引进到编译器技术中,我相信是为了支持ALGOL独一无二的pass-by-name语意。所谓thunk是一小段assembly代码,用来(1)以适当的offset 值调整this指针,(2)跳到virtual function去。例如,经由一个Base2指针调用Derived destructor,其相关的thunk可能看起来是这个样子:
  //虚拟C++代码
  pbase2 dtor thunk:
  this +=sizeof(basel );
  Derived::~Derived(this );
  问题是thunk只有以assembly代码完成才有效率可言。由于cfront使用C作为其程序代码产生语言,所以无法提供一个有效率的thunk编译器。
  Thunk技术允许virtual table slot继续内含一个简单的指针,因此多重继承不需要任何空间上的额外负担。Slots中的地址可以直接指向virtual function,也可以指向一个相关的thunk(如果需要调整this指针的话)。于是,对于那些不需要调整this指针的virtual function(相信大部分如此)而言,也就不需承载效率上的额外负担。
  调整this指针的第二个额外负担就是,由于两种不同的可能:

(1)经由derivedclass(或第一个base class)调用,

(2)经由第二个(或其后继)base class 调用, 同一函数在virtual table中可能需要多笔对应的slots。例如:
  Basel *pbase1 =new Derived;
  Base2 *pbase2 =new Derived;
  delete pbase1;
  delete pbase2;
  虽然两个delete操作导致相同的Derived destructor,但它们需要两个不同的virtual table slots:

1. phasel不需要调整this指针(因为Basel是最左端base class之故,它已经指向Derived对象的起始处)。其virtual table slot需放置真正的destructor地址。
2. pbase2需要调整this指针。其virtual table slot需要相关的thunk地址。
  在多重继承之下,一个derived class 内含n-1个额外的virtual tables,n表示其上一层base classes的个数(因此,单一继承将不会有额外的virtual tables)。对于本例的Derived而言,会有两个virtual tables被编译器产生出来:
1.一个主要实例,与Basel(最左端base class)共享。
2.一个次要实例,与Base2(第二个base class)有关。
  针对每一个virtual tables,Derived对象中有对应的vptr。下图说明了这一点。vptrs将在constructor(s)中被设立初值(经由编译器所产生出来的代码)。
  用以支持“一个class拥有多个virtual tables”的传统方法是,将每一个tables以外部对象的形式产生出来,并给予独一无二的名称。例如,Derived所关联的两个tables可能有这样的名称:
  vtbl Derived; //主要表格
  vtbl Base2 Derived; //次要表格
  于是当你将一个Derived对象地址指定给一个Basel指针或Derived指针时,被处理的 virtual table 是主要表格 vtbl Derived 。而当你将一个 Derived 对象地址指定给一个Base2指针时,被处理的 virtual table 是次要表格 vtbl Base2 Derived 。

由于执行期链接器(runtime linkers)的降临(可以支持动态共享函数库),符号名称的链接可能变得非常缓慢,最慢可到在一部SparcStation 10工作站上,每一毫秒(ms)只处理一个符号名称。为了调节执行期链接器的效率,Sun编译器将多个virtual tables连锁为一个;指向次要表格的指针,可由主要表格名称加上一个offset获得。在这样的策略下,每一个class只有一个具名的virtual table。
  稍早我曾写道,有三种情况,第二或后继的base class会影响对virtual functions的支持。第一种情况是,通过一个“指向第二个base class”的指针,调用derivedclass virtual function。例如:
  Base2 *ptr =new Derived;
  //调用Derived::~Derived
  /r必须被向后调整sizeof( Basel)个bytes
  delete ptr;
  从上图之中,你可以看到这个调用操作的重点:ptr指向Derived对象中的Base2 subobject;为了能够正确执行,ptr必须调整指向Derived对象的起始处。
  第二种情况是第一种情况的变化,通过一个“指向derived class”的指针,调用第二个base class中一个继承而来的virtual function。在此情况下,derived class指针必须再次调整,以指向第二个base subobject。例如:
  Derived *pder =new Derived;
  //调用Base2::mumble()

//pder必须被向前调整sizeof( Basel )个byte

pder->mumble();
  第三种情况发生于一个语言扩充性质之下:允许一个virtual function的返回值类型有所变化,可能是base type,也可能是publicly derived type。这一点可以经由Derived::clone()函数实例来说明。clone函数的Derived版本传回一个Derived class 指针,默默地改写了它的两个base class函数实例。当我们通过“指向第二个baseclass”的指针来调用clone()时,this指针的offset问题于是诞生了:

Base2 *pb1 =new Derived;
  //调用Derived*Derived::clone()
  //返回值必须被调整,以指向Base2 subobject
  Base2 *pb2 = pb1->clone();
  当进行pbl->clone()时,pbl会被调整指向Derived对象的起始地址,于是clone()的Derived版会被调用;它会传回一个指针,指向一个新的Derived对象;该对象的地址在被指定给pb2之前,必须先经过调整,以指向Base2 subobject。

当函数被认为“足够小”的时候,Sun编译器会提供一个所谓的“split functions”技术:以相同算法产生出两个函数,其中第二个在返回之前,为指针加上必要的offset。于是不论通过Basel指针还是通过Derived指针调用函数,都不需要调整返回值;而通过Base2指针所调用的,是另一个函数。
  如果函数并不小,“split function”策略会给予此函数中的多个进入点(entrypoints)中的一个。每一个进入点需要三个指令,但Mike Ball想办法去除了这项成本。对于OO没有经验的程序员,可能会怀疑这种“split function”的应用性,然而OO程序员都会尽量使用小规模的virtual function将操作“局部化”。通常,virtualfunction的平均大小是8行。
  函数如果支持多重进入点,就可以不必有许多“thunks”。像IBM就是把thunk搂抱在真正被调用的virtual function中。函数一开始先(1)调整this指针,然后才(2)执行程序员所写的函数码;至于无须调整的函数调用操作,就直接进入(2)的部分。
  Microsoft以所谓的“address points”来取代thunk策略。即将用来改写别人的那个函数(也就是overriding function)期待获得的是“引入该virtual function之class” (而非derived class)的地址。这就是该函数的“address point”
  

3.虚拟继承下的Virtual Functions

考虑下面的virtual base class派生体系,从Point2d派生出Point3d:
  class Point2d{
  public:
          Point2d( float =0.0,float =0.0);
      virtual ~Point2d();
      virtual void mumble();
      Point2d virtual float z();
      / / ...
  protected:
      Point3d float x, y;
     } ;

class Point3d:public virtual Point2d {
  public:
          Point3d( float =0.0,float =0.0,float =0.0); ~Point3d();
      float z( ) ;
  protected:
      float z;
  } ;
  虽然Point3d有唯一一个(同时也是最左边的)base class,也就是Point2d,但Point3d和Point2d的起始部分并不像“非虚拟的单一继承”情况那样一致。这种情况显示于下图。由于Point2d和Point3d的对象不再相符,两者之间的转换也就需要调整this指针。至于在虚拟继承的情况下要消除thunks,一般而言已经被证明是一项高难度技术。

当一个virtual base class从另一个virtual base class派生而来,并且两者都支持virtual functions和nonstatic data members时,编译器对于virtual base class的支持简直就像进了迷宫一样。虽然我有一个以上的算法可以决定适当的offsets以及各种调整,但这些素材实在太过迷离,不适合在此处讨论!我的建议是,不要在一个virtual base class中声明nonstatic datamembers。

3.C, S, and D语意学

       

1.1 pure虚函数

C++新手常常很惊讶地发现,一个人竟然可以定义和调用(invoke)一个pure virtual function;不过它只能被静态地调用(invoked statically),不能经由虚拟机制调用。例如,你可以合法地写下这段代码:
  //:定义pure virtual function
  // 但只可能被静态地调用(invoked statically)
  inline void
  Abstract_base:: interface( ) const//这是一个pure virtual const
  function
  //……
  }
  

inline void
  Concrete_derived:: interface( ) const//

{
  //:静态调用(static invocation)
  Abstract_base:: interface( );
  //能够调用一个pure virtual function
  // …
  }

要不要这样做, 全由class设计者决定。唯一的例外就是pure virtual destructor: class设计者一定得定义它。为什么?因为每一个derived class destructor会被编译器加以扩张,以静态调用的方式调用其“每一个virtual base class”以及“上一层base class”的destructor。因此,只要缺乏任何一个base class destructors的定义, 就会导致链接失败。

你可能会争辩说,难道对一个pure virtual destructor的调用操作,不应该在“编译器扩张derived class的destructor”时压抑下来吗?不!class设计者可能已经真的定义了一个 pure virtual destructor(就像上一个例子中定义了 Abstract_base::interface()那样)。这样的设计是以C++语言的一个保证为前提:继承体系中每一个class object的destructors都会被调用。所以编译器不能够压抑这一调用操作。也可能争辩说,难道编译器不应该有足够的知识“合成” (如果class 设计者忘记定义或不知道要定义)一个pure virtual destructor的函数定义吗?不!编译器的确没有足够知识,因为编译器对一个可执行文件采取“分离编译模型”之故。是的,开发环境可以提供一个设备,在链接时找出pure virtual destructor不存在的事实,然后重新激活编译器,赋予一个特殊指令(directive),以合成一个必要的函数实例;但是我不知道目前是否有任何编译器这么做。
  一个比较好的替代方案就是,不要把virtual destructor声明为pure。

1.2虚拟规格的存在(Presence of a Virtual Specification)

  如果你决定把 Abstract_base:: mumble( )设计为一个 virtual function,那将是一个糟糕的选择,因为其函数定义内容并不与类型有关,因而几乎不会被后继的derived class改写。此外,由于它的non-virtual 函数实例是个inline函数,如果常常被调用的话,效率上的报应不可谓不轻。
  然而,编译器难道不能够经由分析,知道该函数只有一个实例存在于class层次结构之中吗?果真如此的话,难道它不能够把调用操作转换为一个静态调用操作(static invocation),以允许该调用操作的inline expansion吗?如果class层次结构陆续被加入新的classes,内有该函数的新实例,又当如何?是的,新的class会破坏优化!此函数现在必须被重新编译(或是产生第二个——也就是多态——实例,编译器将通过流程分析决定哪一个实例要被调用)。不过,函数可以二进制形式存在于一个library中。欲挖掘出这样的相依性,有可能需要某种形式的persistentprogram database或library manager。
  一般而言,把所有的成员函数都声明为virtual function,然后再靠编译器的优化操作把非必要的virtual invocation去除,并不是好的设计理念。

1.3虚拟规格中const的存在

决定一个virtual function是否需要const,似乎是件琐碎的事情。但当你真正面对一个abstract base class 时,却不容易做决定。做这件事情,意味着得预期subclass实例可能被无穷次数地使用。不把函数声明为const,意味着此函数不能够获得一个const reference或const pointer。比较令人头大的是,声明一个函数为const,然后才发现实际上其derived instance必须修改某一个data member。我不知道有没有一致的解决办法,我的想法很简单,不再用const就是了。
重新考虑class的声明
  由前面的讨论可知,重新定义 Abstract_base 如下,才是比较适当的一种设计:
  class Abstract_base{
  public:
      virtual~ Abstract_base( ); //不再是pure virtual
      virtual void interface()=0;//不再是const
      const char* mumble( ) const{ return mumble;}//不再是 virtual protected:
      Abstract base( char* pc=0);//新增一个带有唯一参数的 constructor char* _mumble;
} ;

后记:首先再次感谢何佳雨同学的part2

参考资料: 《c++ primer plus》  《c++ primer》 《ec1 》 《ec isad》

另外强调,大学期间欲精进语言者务必读不下于4本外文经典书籍,包括入门(700页为宜)进阶,提升,探索四个方面,如此也只是完成了优秀水平所要求的五分之一。

C(C++)后端基础 五万字浅析指针相关推荐

  1. [Python从零到壹] 五.网络爬虫之BeautifulSoup基础语法万字详解

    欢迎大家来到"Python从零到壹",在这里我将分享约200篇Python系列文章,带大家一起去学习和玩耍,看看Python这个有趣的世界.所有文章都将结合案例.代码和作者的经验讲 ...

  2. 五万字总结,深度学习基础。

    文章目录 1 基本概念 1.1 神经网络组成? 1.2 神经网络有哪些常用模型结构? 1.3 如何选择深度学习开发平台? 1.4 为什么深层神经网络难以训练? 1.5 深度学习和机器学习的异同? 2 ...

  3. [系统安全] 四十四.APT系列(9)Metasploit技术之基础用法万字详解及防御机理

    您可能之前看到过我写的类似文章,为什么还要重复撰写呢?只是想更好地帮助初学者了解病毒逆向分析和系统安全,更加成体系且不破坏之前的系列.因此,我重新开设了这个专栏,准备系统整理和深入学习系统安全.逆向分 ...

  4. c++ ftp服务端_重磅干货||五万字长文总结:C/C++ 知识(下篇)

    结识更多同行,共同讨论"嵌入式"技术.欢迎添加社区客服微信,备注发送"电源+公司名(学校)+职位(专业)"拉您入群. 回顾上篇:五万字长文总结:C/C++ 知识 ...

  5. 熬夜整理,五万字长文总结 C/C++ 知识点

    这是一篇五万字的C/C++知识点总结,长文预警.能滑到留言区的,都是麒麟臂! 学C/C++的同学,收藏起来哦~ C/C++ 知识总结 目录 C/C++ STL 数据结构 算法 Problems 操作系 ...

  6. 五万字长文总结 C/C++ 知识

    来自:GitHub - huihut 链接:https://github.com/huihut/interview#cc C/C++ 知识总结 这是一篇五万字的C/C++知识点总结,包括答案,需要的同 ...

  7. 重磅干货 | 五万字长文总结 C/C++ 知识(下)

    置顶/星标公众号????,硬核文章第一时间送达! 链接 | https://github.com/huihut/interview 回顾上篇:<重磅干货 | 五万字长文总结 C/C++ 知识(上 ...

  8. [Python从零到壹] 十四.机器学习之分类算法五万字总结全网首发(决策树、KNN、SVM、分类对比实验)

    欢迎大家来到"Python从零到壹",在这里我将分享约200篇Python系列文章,带大家一起去学习和玩耍,看看Python这个有趣的世界.所有文章都将结合案例.代码和作者的经验讲 ...

  9. [Python从零到壹] 九.网络爬虫之Selenium基础技术万字详解(定位元素、常用方法、键盘鼠标操作)

    欢迎大家来到"Python从零到壹",在这里我将分享约200篇Python系列文章,带大家一起去学习和玩耍,看看Python这个有趣的世界.所有文章都将结合案例.代码和作者的经验讲 ...

最新文章

  1. linux c 函数专挑,Linux C wait函数
  2. 数据加密_2021年数据加密的六大趋势
  3. 联想y470上三代cpu_AMD三代线程撕裂者首测 单核不再是问题(二)
  4. 计算机信息安全技术计算题,计算机信息安全技术练习题.doc
  5. mongodb从3.2升级到4.4_人教版六年级下册数学微课视频及练习4.4.2 比例尺的应用...
  6. struts标签 s date 的使用
  7. 复旦nlp实验室 nlp-beginner 任务二:基于深度学习的文本分类
  8. 常用工具软件使用【2】
  9. 小米id锁状态查询_怎么通过序列号查询苹果手机真伪
  10. 龙芯2h芯片不能进入pmon_基于龙芯2F架构的PMON分析与优化
  11. Google Maven Replacer Plugin插件详解
  12. 74cms|骑士cms|开源招聘系统,数据结构
  13. 关于stm32的VCP技术原理
  14. Redis6简单安装
  15. 1118 - Row size too large (> 8126). Changing some columns to TEXT or BLOB or using ROW_FORMAT=DYNAMI
  16. 现在AI发展到什么阶段了
  17. 2000-2020全国及31省城投债数据
  18. 2021-06-04 wms仓库管理常见的问题
  19. 白酒行业跌宕起伏,资本在打什么算盘?
  20. centos 6下apache kudu安装报错Error during hole punch test问题解决

热门文章

  1. CSS--抽屉(dig.chouti.com)页面
  2. 什么是DDL与DML
  3. 惠州学院采购JKTD-1000型铁电材料测试仪
  4. Java生成证书用HTTPS进行访问
  5. 关于汇编语言学习的环境配置及使用方法
  6. Hilt 介绍 | MAD Skills
  7. 报错信息(AUTOINCREMENT is only allowed on an INTEGER PRIMARY KEY).
  8. 3D角色硬表面建模技巧与思路分享【下】
  9. 日语2级终于过了,在大学的最后一年
  10. 忍3服务器维护奖励,《忍者必须死3》重新开服补偿有哪些 重新开服补偿一览