[后台开发工程师总结系列] 1.C++

数组

1. 一维数组的声明

一维数组的声明应指出以下几点

  1. 储存在元素中的值类型 2. 数组名 3. 数组中的元素数

数组中定义的类型可以使内置类型或类类型,除引用外,数组元素还可以是符合类型,但是不能定义引用。

虽然没有引用数组,但是可以有数组引用

1
2
int a[6] = {0,2,4,6,8};
int (&p)[6] = a;

2. 一维数组的初始化

在定义数组时,可以为元素提供一组用逗号分隔的初值,称为初始化列表。数组元素若没有被显示初始化,就会被像普通变量一样初始化。

  1. 函数体外定义内置数据类型,元素初始化为0
  2. 函数体内定义内置数据类型,元素无初始化
  3. 如果不是内置类型,不管定义在哪里都会调用构造函数,没有构造函数报错

数组大小未知,可以用C++风格的一维数组动态声明

1
2
3
int* a = new int[n];

delete[] a;

3. C风格的字符串与字符数组

C风格字符串包含两种

1 字符串常量 以双引号扣起来的字符序列是字符串常量

2 末尾添加了”\0” 的字符数组

C++中有很多字符串处理函数(strcpy, strcat)传递给这些函数的参数必须有非零值,且指向以NULL结束的字符数组。

4. 二维数组

二维数组是最常用的高维数组,包含了数据行和列

1
2
3
4
5
6
7
int ia[3][4]={
{0,1,2,3},
{4,5,6,7},
{8,9,10,11}
};
// 或顺序初始化
int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};

本质上讲,所有数组在内存中都是一维线性的,不同的语言储存方式不同。

C++中采取了行优先的存储方式

5. 数组指针、指针数组与数组名的指针操作

C++常常把地址当成整数来处理,但这不意味着程序员可以进行算术操作。

C++的指针运算一般包含两种形式,第一种形式是 指针 + - 整数 ,在C++中这种操作表示走动几个元素的位置。

1
2
3
4
5
6
7
void main(){
int i = 11;
int const *p = &i;
p++;
printf("%d", *p);
}
// 运行结果是一个 Garbage value

第二种类型是指针运算有以下形式:指针 - 指针

得到是字符间的鲁丽,而且是以数组长度为单位,(如果两指针不在一个数组,结果未定义)

6. 指针数组与数组指针

所谓指针数组,是指一个数组里面装着指针。即指针数组是一个数组。一个有10个指针的数组如下定义

1
int* a[10];

所谓数组指针,知识一个指向数组的指针,一个指向10个元素的数组指针定义为

1
int (*p)[10];

7. 线性表的线性存储

一维数组可用来实现线性表的顺序存储

线性表的储存顺序又称顺序表,其中,线性表是逻辑概念(一一对应)而顺序表和链表是储存结构,二者属于不同层面的概念

顺序表最主要的特点是随机存取,及通过首地址和元素号在O(1) 的时间找到指定的元素,但是插入和删除需要大量操作。其有n+1 个插入点,平均时间复杂度是 n/2, 删除的平均时间复杂度是O(n+1/2)

字符串

1. 字符串标准函数

函数名 含义
strlen(s) 返回s的产犊,不包括最后的空字符串 null
strcmp(s1, s2) 比较两个字符串是否相同,相等返回0; s1大于s2 返回整数,否则返回负数
strcat(s1, s2) s2 拼接拼接到s2 后 返回s1
strcpy(s1, s2) 将s2 赋值给s1, 并返回

以上函数的简要实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int strlen(const char *str){
assert(str!=NULL);
int len = 0;
while((*str++) != '\0') len++;
return len;
}
int strcmp(const char* str1, const char* str2){
assert(str1!=NULL && str2!=NULL);
int ret = 0;
while(!(ret=*(unsigned char *)str1 - *(unsigned char*)str2) && *str1){
str1++;
str2++;
}
if(ret<0) ret = -1;
else if(ret>0) res = 1;
return ret;
}
char* strcat(char* strDest, const char* strSrc){
char* address = strDest;
assert(strDest!=NULL&&strSrc!=NULL);
while(*strDest) strDest++;
while(*strDest++=*strSrc++);
return address;
}
char* strcpy(char* strDestination, const char* strSource){
assert(strDestination!=NULL && strSource!=NULL);
char* strD = strDestination;
while((*strDestination++=*strSource++) != '\0');
return strD;
}

2. memcpymemset

memcpy 功能:从源 src 所指向的内存地址开始拷贝n个字节到目标dest地址所存地址的起始位置。

memset功能:将s中前n个字节用ch替换并返回s,该方法是对较大结构体、数组较快的清零方法。

1
2
3
void* memcpy(void* dest, const void* src, size_t n);

void* memset(void* dest, int ch, size_t n);

strcpy 与 memset 的区别

  1. 复制的内容不容,strcpy只能复制字符串,而memcpy可以复制任何内容,而且strcpy还会复制结束的’\0’
  2. 复制的方法不同,strcpy不需要指定长度,遇到 \0 结束, 而memcpy有第三个参数限制长度

结构体、共用体、枚举

1. 结构体

2. 共用体

结构体和共用体都由不同的数据结构组成,但是在同一时刻,共用体只存放了一个被选中的数据成员,对于共用体中成员的赋值,会导致其他成员的重写,原来的值就不存在。

共用体的这个特性常与大端、小端一起考察,

在操作系统中,x86和一般的OS(如windows,FreeBSD,Linux)使用的是小端模式。但比如Mac OS是大端模式。

大端储存格式是字节高在低地址中,而小端相反

1551074805277

1
2
3
4
5
6
7
8
9
union Student{
int i;
unsigned char ch[2];
}
int main(){
Student student;
stuednt.i = 0x1420;
printf("%d, %d", student.ch[0], student.ch[1]);
}

3. sizeof 运算符

sizeof 是一个单目运算符,就像其他++ – 一样,它并不是函数,sizeof 以字节形式给出储存的大小,操作数可以使一个表达式或类型名,而且sizeof发生在编译时,忽略括号中的各种计算

1
2
3
4
5
6
7
8
char ca1[] = {'C', '+', '+'};  // strlen(ca1) = 未定义 sizeof(cha1) = 3
char ca2[] = {'C', '+', '+', '\0'}; //strlen(ca2) = 3 sizeof(ca2) = 4

int a[10] ; // sizeof(a) = 40
char b[] = "hello"; // sizeof(b) = 6
int *c = new int[50]; // sizeof(c) = 4

int (*a)[10]; // sizeof(a) = 4;

4. struct的空间计算

关于struct的笔试题比较多,struct计算较为复杂,总体遵循两个原则

  1. 整体占用空间是 最大成员所占字节数的整数倍 ,
  2. 数据对齐原则, 排到成员变量时,前面拜访的大小必须是该类型大小的整数倍
1
2
3
4
5
6
7
8
9
10
11
12
struct s1{
char a;
double b;
int c;
char d;
}; // 24
struct s2{
char a;
char b;
int c;
double d;
}; //16

预处理器、作用域、static、const 以及内存管理

1. C预处理器

C语言预处理器在编译器之前运行 ,主要包括

  1. 宏定义与宏替换 2 文件包含 3 条件编译

宏是借用汇编语言的概念,为C语言程序中方便做一些定义和扩展,这些语句以define开头,

1
#define max_v 1000

由于预处理在编译之前进行,而编译的任务之一是语法检查,所以预处理不做语法检查、不分配内存

1
2
#include <standard_header>
#include "my_file.h"

如果定义在尖括号 <> 里, 那么认为该头文件是标准头文件。编译器会在预定位置搜索这些文件,如果文件名在一对引号里,那么是非系统头文件,查找源于源文件坐在的路径。

2. 全局变量与局部变量

全局变量也被称为外部变量,他在函数外部定义,不属于哪个函数,它属于一个源程序文件,作用域是整个个源程序。 引用一个全局变量有两种方式:引用头文件、extern 两种方式

1
2
3
4
5
6
// file1.cpp
int count = 1;

// fiel2.cpp
extern int count;
count++;

3. STATIC

不考虑类,static 的作用主要有三条:

  1. 隐藏

当同事编译多个文件时,所有为加static前缀的变量和函数都具有全局可见性

  1. static默认初始化为0, 包括未初始化的全局静态变量和局部静态变量。静态变量和全局变量都储存与BSS段中,BSS段中所有字节默认值都为0,
  2. 保持局部变量内容的持久 函数内部的局部变量,调用时存在,退出时消失,但是静态局部变量定义后就一直存在着。值的注意的是,他虽然存在,但是退出作用域后依然不能调用。

1551318177523

4. 类中static中的作用

C++重用了static这个关键字,并赋予了与之前不同的含义:表示属于一个类而不属于这个类任何对象的变量或函数(和java一样)

static独立于类的对象存在

1
2
3
4
5
6
7
8
9
10
11
class Account{
public:
void applyint(){ amout += amount * interRate}
static double rate() {return interRate;}
static double rate(double);
private:
std::string owner;
double amount;
static double interRate;
static double initRate();
}
  • 静态数据成员

在内类数据成员声明前加static, 该数据成员就是类的静态数据成员。通常,非static数据成员存在于每个对象中。static 数据成员独立于类的任何对象而存在;每个与static数据成员是与类关联的对象,并不与该类相关联,也就是当某个类的实例修改了静态成员变量,修改值被所有类可见

静态数据成员也存在全局区,静态数据成员定义时要分配空间,所以不能在类中声明,static数据成员必须在类的定义体外部定义正好一次

规则由例外, const static 基本整形可以在定义体中初始化。

类中数据成员的布局情况是:

  1. 非静态成员在类对象中排列顺序和生命顺序一致,其在任何声明的静态成员都不会被放进静态布局中
  2. 静态数据成员放在全局中,和类对象无关。

5. 静态成员函数

静态数据成员与静态成员函数一样,都是类的内部实现,属于类定义的一部分,因为它为类服务而不是为某一个类服务。因为普通的成员函数属于某个类的对象,所以普通的成员函数一般隐含了一个this指针,这个this指针指向类对象本身。

但是与其他普通成员函数比,静态成员函数不与任何对象关联,因此它不具有this指针,因而它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数 他只能调用其他的静态成员函数与静态数据成员。

因为static成员不是任何对象的组成部分,它不能被声明为const, 也不嗯呢该被声明为虚函数、volatile

静态函数被总结为以下几点:

  1. 静态成员之间可以互相访问,包括静态成员函数访问静态成员、函数。
  2. 非静态成员函数 可以任意非访问静态、或非静态成员函数、成员
  3. 由于没有this指针的开销,静态函数相比非静态函数略快

6.const

定义

C++ 中 const限定符 把一个对象转换成一个常量

1
const int buffSize = 512;

常量修改后就不能被修改,所以定义时必须初始化。

在全局作用域里定义非const变量时,它在整个程序中都可以访问。与其他变量不同,除非特别说明,const变量定义该对象文件的局部变量,不能被其他文件访问。而定义的extern关键字可以使其在外部访问。

const在 C和C++中的区别

常量引进是在早起的C++中,当时标准正在制定。C中const意思是 “一个不能被改写的普通变量”因而它总是占用存储。 C中的const是内部连接,而C++中默认const是外部连接。这样C++中完成相同的事就需要改成外部连接。

const最初提出是取代#define,其有几个优点

  1. const 常量有数据类型
  2. 常量可能会比define产生更小的目标代码
  3. const 可以执行常量折叠
指针和 const 修饰符
1
2
3
4
5
6
7
// const 对象指针
// 指针指向的对象不可变
const double *cptr;

// const 指针
// 指针 指向不可变
double* const cptr;
修饰参数和返回值

const最具威力的用法是对函数声明的应用,在一个函数式声明内,const可以和函数返回值、参数、函数自身产生关联

  • const修饰返回值

若返回值是值类型,则对于内部数据类型来说,返回值是常量并没有关系

  • const修饰函数参数

如果函数值传递,可用const限制函数参数。这是明确告诉编译器这个值不会也无法改变。由于是传值,这种约定对于调用者意义不大,然而若是在函数参数使用引用,函数可能会接受临时对象。const保证了该引用的值在函数运行过程中不会被改变。

  • const在类中的使用

const成员函数

1
2
3
4
class base{
void func1();
void func2() const;
}

上述代码中,func2 是base常量的成员函数,func2函数末尾声明const隐含了this形参的类型

const施加于成员函数的目的,是为了确保成员函数可以作用域const对象上,const对象、指针、指向const对象的指针或引用只能调用非const成员函数。

  • C++中说明 static、const、static、const成员变量的初始化

C++中,static静态成员变量不能在类内初始化,在类内部只是声明,定义必须在类定义体的外部,通常在类的实现文件中初始化,static关键字只能用于类定义体内部的声明中,定义时不能标注为static

在C++中, const成员变量也不能在类定义处初始化,只能通过构造函数初始化列表进行,并且必须有构造函数。const数据成员只在某个对象的生存期是常量,而整个类而言是可变的。因为类可以创建多个对象,不同对象其const值可以不同,所以不能在类的声明中初始化const成员。

内存的管理与释放

一个c++程序,内存主要包含以下部分,栈区、堆区、全局(静态)储存区、文字常量区、代码区

Linux 程序内存空间布局

下图是一个典型的内存空间布局

1546000621186

  1. 代码段 通常指存放程序执行代码的一块内存区域。这部分大小在程序运行前已经确定,并且内存区域只读,某些架构也允许可写
  2. 初始化数据段,存放程序中已初始化的全局变量
  3. 未初始化数据段, 未初始化全局变量的一块区域
  4. 堆 堆用于储存程序运行时动态分配的内存段,它的大小不确定,可以动态的扩张或缩减,程序调用malloc及free来动态分配内存,当进程调用malloc、free时,新配的内存被动态添加到堆上或删去
  5. 栈 存放程序的局部变量,并且用户函数调用的传参和返回

堆栈的区别

1 申请方式不同

栈: 系统自动分配,声明在函数中一个局部变量;系统自动在栈中为其分配空间

堆: 需要程序员自己申请。并指明大小

2 申请后系统的相应不同

栈: 只要栈的剩余空间大于申请空间,系统便提供内存,否则报异常

堆: 操作系统有一个记录空闲内存的链表,系统受到申请会遍历链表去找一块内存,从空间链表中删去,并将剩下的空间再放回去。

3 申请大小的限制不同

栈: 栈向低地址扩展,是一块连续的区域,栈顶的地址和栈的最大容量是系统规定好的 10M

堆: 堆是低地址向高地址扩展,不连续的内存区域 理论32 位系统可以有 4-1 = 3G的空间

4 申请效率不同

栈由系统自动分配,速度较快,程序员无法控制

堆是由new分配、速度慢,容易产生碎片,但是方便

  • 堆和栈的区别

栈区由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈,速度较快。

堆区一般由程序员分配释放,若程序员不释放,程序结束后由操作系统回收。注意他和操作系统中的堆不一样,分配方式类似于链表,速度较慢且易产生碎片,不过用起来方便

1
2
3
4
// C语言
char* p1 = (char *)malloc(10);
// C++
char *p2 = new char[10];
  • malloc,free和 new, delete 的区别

相同点:都可以动态的申请、释放内存

不同点:

  1. 操作对象不同。

malloc和free是库函数,不是运算符,不在编译器控制权限之内,无法构造和析构。

new的执行过程是,首先调用 opreator new 的标准库函数,分配足够大的原始未知类型的内存,以保存指定类型的一个对象,接下来运行该类型的一个构造函数,用指定的初始化方式构造对象,最后返回新构造对象的指针。

delete执行过程是 首先指向对象的析构函数,然后调用opreator delete标准函数释放内存。

  1. 用法上不同

malloc的返回值是void*, 所以调用malloc需要进行显示转换,转换成需要的指针类型

malloc函数本身不识别内存是什么类型,他只关心字节大小

free(p)释放内存,如果p是NULL指针,那么free多少次都不会出问题,但是如果p不是null指针,对p的两次free就会出问题。

总结如下:

  1. malloc free 是C++库函数,new delete是运算符
  2. new自动计算分配空间,而malloc需要手工计算
  3. new类型安全的,而malloc不是
  4. new调用opreator new分配空间、调用构造函数、而malloc不调用构造函数。delete调用析构函数,然后operator delete, 释放实例的空间
  5. malloc,free 需要库函数支持, new/delete 不需要
  • 什么是声明周期、作用域、全局变量、静态变量、局部变量、const变量生命周期

    | 类型 | 作用域 | 生命周期 | 内存布局 | 定义方法 |
    | ———— | ———————————- | —————— | —————— | ————————– |
    | 全局变量 | 全局作用域(只在一个源文件中定义) | 程序运行中一直存在 | 全局(静态)存储区 | |
    | 全局静态变量 | 文件作用域 | 程序运行一直存在 | 全局(静态)存储区 | static 关键字 const 关键字 |
    | 静态局部变量 | 局部作用域 | 程序运行期一直存在 | 全局(静态)存储区 | 局部static定义 |
    | 局部变量 | 局部作用域 | 程序出局部即被销毁 | 栈区 | auto 或省略 |

    函数

函数是有名字的计算单元,对程序的结构化至关重要。

C++中,函数原型就是函数的声明。所以函数除了向用户说明如何使用以外,还告诉编译器存在这样一个可以使用的函数。函数的声明同变量一样,是一个语句,函数的定义为返回类型、函数名、形参表、函数体组成。

1. 参数传递

函数的参数分为形参和实参两种。

形参出现在函数的定义中,整个函数体都可以使用,离开函数不能使用。实参出现在主调函数中,被调入函数后实参也不能使用。C++有三种传值方式:值传递、指针传递、引用传递

给函数传递实参遵循变量的初始化规则,非引用类型以相应的实参副本初始化,对形参的修改只作用域副本,为了避免副本的开销,可以将参数指定为引用类型。任何对引用类型的修改都会影响实参值本身

引用传递有以下特点:

  1. 传递引用给函数,这时被调函数的形参就作为原来主调函数中实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应目标对象的操作。
  2. 使用应用传递函数的参数,在内存中没有实参的副本,它是直接对实参尽心该操作。函数调用时,需要给形参分配存储单元,形参是副本;如果传递的是对象,还将调用拷贝构造函数。
  3. 指针作为函数参数虽然也能达到引用的效果,但是在被调函数中同样要给形参分配存储单元

2. 内联函数

内联函数一般用inline修饰,一般有两种

  1. 成员函数为内联函数

在类中定义的成员函数全部默认为内联函数,可以显式加上inline标识符。

  1. 普通函数成为内联函数

普通函数添加inline关键字称为内联函数。

通常编译时,内联函数不进行调用,而是函数体替换成函数名。内联扩展可以消除调用时的时间开销

3. 函数重载

函数重载是指在同一块作用域内,可以有一组相同的函数名,不同的参数列表,这组函数被称为重载函数。重载函数通常被命名为一组功能相似的函数,减少了函数名的数量。

4. 函数的模板与泛型

首先介绍泛型编程的概念。所谓泛型编程就是独立于任何特定的方式编写代码。使用泛型时,需要提供程序实例所操作的值或类型。泛型编程和面向对象编程一样,都依赖于某种形式的多态性。

面向对象编程的多态性应用于存在于继承关系的类,我们能够使用这些类的代码,忽略基类与派生类之间的类型差异。只要使用基类的指针或引用,基类、派生类对象就可以使用相同的代码。

5. 函数模板

函数模板定义以template关键字开始,后接模板形参表,模板形参表用尖括号括住一个或多个模板形参的列表,形参之间以逗号分隔。

1
2
3
4
template <typename T>
void callWithMax(const T &a, const T &b){
f(a>b?a:b);
}

形参关键字跟在关键字class或 typename 之后,这里几乎没有区别。

6. 引用

引用就是对象的另一个名字,所谓引用其实是一个特殊的变量,这个变量的内容是绑定在这个引用上面对象的地址,而使用这个变量时,系统就会自动根据这个地址去找它绑定的变量,然后再对变量进行操作。所以本质上说引用还是指针,只不过这个指针是不能被修改的,任何时候他的操作都会发生到他指向的指针身上。所以说C++中引用一旦定义,就必须将他与一个变量绑定起来,且不能修改这个绑定

  1. 引用不能为空,引用被创建时它必须被初始化。而指针可以为空值
  2. 一旦引用被定义就不能被修改,而指针可以随时修改
  3. 不可能有空引用
  4. sizeof(引用) 得到的是指向变量的大小, 指针得到的指针本身的大小
  5. 引用复制修改是直接修改这个引用关联的对象值
  6. 引用使用时不需要解引用。而指针需要解引用
  7. 如果返回动态对象或内存,必须使用指针,否则可能引起内存泄露

1. 访问标号

访问标号public、private、protected 多次出现在类定义中,给定的访问标号应用到下一次出现为止

2. 类成员简介

* 成员函数

声明成员函数是必须的,但是定义成员函数是可选的,类内定义默认为inline

调用成员函数时,实际上使用对象调用,每个额成员函数都有一个隐含的形参的this 。 在调用成员函数时,this初始化为函数的地址。

* 构造函数

构造函数时特殊的成员函数,与其他成员函数不同,构造函数和类同名,而且没有返回类型。一个类可以有多个构造函数,每个构造函数必须有与其他构造函数不同的类型或形参。如果一个类没有显示的定义任何构造函数,编译器将自动为这个类生成默认构造函数。

若使用编译器自动生成的默认构造函数,则类中变量按照初始化变量的规则初始化。

C++中,成员变量的初始化顺序与变量在类型中的声明顺序相同,而与他们在构造函数中初始化列表中的顺序无关。

* 拷贝构造函数

拷贝构造函数、赋值操作符、析构函数 总称为赋值控制,编译器自动实现这些操作。

如果累需要析构函数、则它也需要赋值操作符、拷贝构造函数。这是一个有用的经验法则, 被称为三法则。它的含义是,如果有析构函数,就需要所有的三成员。

通常编译器合成拷贝构造函数十分精炼–只做必要的工作。但是某些类依赖默认定义会导致灾难。

只有单个形参,而且形参是对该类型的引用(常常加const),这样的构造函数被称为 拷贝构造函数。该函数有以下作用

  1. 根据另一同类型对象初始化一个对象
  2. 复制一个对象,将他作为实参传递给一个函数。
  3. 初始化顺序容器中的元素
  4. 根据元素初始化列表初始化元素数组
*浅拷贝与深拷贝

浅拷贝:被复制对象的所有变量都与原来有相同的值,而所有其他对象的引用还指向元对象,仅仅复制对象,不复制其引用对象

深拷贝:被复制的对象都与原对象有相同的值,除去其他对象的变量。引用其他对象变量指向被复制过的新对象,而不是原有对象。(换言之把要复制对象的应用对象全部复制了一份)

1
2
3
4
5
6
7
8
9
10
struct Test{
char *ptr;
}
void shallow_copy(Test &src, Test &dest){
dest.ptr = src.ptr;
}
void deep_copy(Test &src, Test &dest){
dest.ptr = malloc(strlen(str.ptr)+1);
memcpy(dest.ptr, src.ptr);
}
*析构函数

构造函数的用途是自动分配资源。构造函数可以打开缓冲区或文件,在构造函数分配了资源以后,需要一个对应的操作回收、自动释放资源。析构函数就是这样一个特殊的函数。作为构造函数的补充,对象超出动态分配的作用域或被删除时,自动应用析构函数。

构造函数不能被定义为虚函数,但是析构函数可以被定义为虚函数。

* 方法覆盖、重写

覆盖是指:派生类覆盖类中的同名函数,要求基类函数必须是虚函数

  1. 与基类虚函数有相同的参数个数
  2. 与基类虚函数有相同的参数类型
  3. 与基类虚函数有相同的返回类型

覆盖和重写是子类和父类之间的关系,是垂直关系。而重载是同一个类中不同方法之间的关系。

* 方法隐藏

隐藏是指在某些情况下,派生类函数屏蔽了同名函数。

  1. 如果两个函数参数相同,基类不是虚函数(与重写的区别是是否是虚函数)
  2. 两个函数参数不同,不论是否虚函数都会被屏蔽

面向对象

1. 继承权限

通过继承机制,可以利用已有数据类型定义新的数据类型。所以定的新数据类型不仅有新定义成员,还有旧成员。已存在的类称为父类、或基类。而派生出的类称为派生类或子类。

继承可以有多继承,而这些基类有一个共同的基类,则在最低层的派生类中会保留这个间接共同基类成员的多份同名成员。为了解决这个问题,提出了虚继承。虚继承时,公共基类在对象中只有一份拷贝。

继承的访问权限

1551232308099

值的注意的是,派生类对象和派生类中成员函数对基类的访问权限是不同的。

2. 继承二义性

类指针的转换规则是:

  1. 共有继承时,派生类对象、对象指针、对象引用 可以赋值给基类的对象、指针、引用 (隐式转换)
  2. C++允许把基类指针显示转换成派生类的指针或引用
  3. 一个指向基类指针可以用来指向该基类公有派生类的任何对象,这是C++动态性实现的关键。
3.多重继承和菱形继承

当继承基类时,派生类获得了基类所有数据成员的副本。如果这时进行多继承,类会包含两个类的子对象。

一般来说,派生类对基类的访问应当具有唯一性,但是多继承时,编译器无法判断数据成员,这就是二义性问题。二义性问题可以通过定义一个同名函数,对父类同名函数进行隐藏来解决。

菱形继承 可以通过虚基类解决。

4. 虚函数多态

多态性是面向对象语言的基本特征,仅仅是将数据和函数绑在一起,封装、继承都不能真正的了解面向对象的设计思想。多态是面向对象语言的精髓。多态性可以被概括为:“一种接口, 多种方法”,前面讲过函数重载是一种简单的多态,一个函数名对应着几个不同的函数原型。

更通俗的说,多态是统一个操作作用于不同的对象会有不同的响应;多态分为静态多态和动态多态。函数重载和运算符重载是静态的多态,虚函数属于动态的多态。

静态和多态联编

程序调用函数时, 具体使用哪个模块是编译器决定的。以函数重载为例,C++编译器根据传递给函数的参数和函数名来决定具体使用哪个函数,称为联编或绑定。编译器可以在编译过程中实现这个联编,在编译过程中进行的联编叫做静态联编、或早期联编。

在一些场合下,编译器无法在编译过程中完成联编,必须在程序运行时选择,因此编译器必须提供一套“动态联编”的机制,也叫晚期联编。C++通过虚函数实现动态联编。

虚函数的定义

虚函数定义很简单,加virtual即可

如果一个基类的成员函数被定义为虚函数,那么他在所有派生类中保持为虚函数;即在派生类中省略了virtual关键字,也仍然是虚函数。

派生了虚函数有要求:

  1. 与基类虚函数有相同的参数个数
  2. 与基类虚函数有相同的参数类型
  3. 与基类的虚函数有相同法返回类型

即除函数体完全相同,由之前的定义,其中有不相同的定义,即被认为是函数隐藏。

虚函数的访问

和普通函数一样,虚函数一样可以通过对象名来访问,此时编译器采用静态联编。通过对象名访问虚函数时,调用哪个类取决于定义对象名的类型。对象类型是基类时,调用基类函数;对象类型是子类时,调用子类函数。

使用指针访问非虚函数时,编译器根据指针类型来决定调用的函数,而不是根据指针指向的对象类型

使用指针访问虚函数时,编译器根据指针指向的类型来决定调用的函数(动态联编),而与指针本身的类型无关

引用访问与指针访问类似,不同的是引用一经声明其调用函数就不会被改变。引用可以作为限制的指针。

总结如下, C++默认不触发动态绑定,触发条件有2:

  1. 只有指定为虚函数的成员函数才能动态绑定,成员默认为非虚函数
  2. 必须通过基类类型的指针或引用进行访问。

构造函数为什么不能是虚函数

假设A是父类,B是子类,则构造函数的顺序是 A -> B

而根据虚函数的性质,如果构造函数是虚函数, 一个声明A类的指针去指向B类,该类初始化时需要先找B类的构造函数 B -> A 这样产生了循环调用

虚函数表指针 (vptr)及 虚基类表指针(bptr)

见下文 C++对象模型

纯虚函数

许多情况下,基类不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给基类的派生类去做,这就是纯虚函数的作用。

纯虚函数可以让类有一个操作名称而没有操作内容,让派生类继承时再具体的给出定义。凡是含有纯虚函数的类称为抽象类,这种类不能声明对象,只是作为基类为派生类服务。除非在派生类中完全实现所有的纯虚函数。否则派生类也是抽象类,不能实例化对象。

简单对象模型对比

简单对象模型

第一个模型十分简单,它可能为了降低 C++ 编译器设计复杂度而开发出来,而空间、执行效率较低。在这个简单的模型中,一个objects是一系列的slots, 每一个slots指向一个menbers。

1551323006541

在这模型中,members 本身不在objects中,只有指向members的指针才放在objects内,这样可以避免 members不同类型而需要不同空间所招致的问题。这个模型并没有被引用,不过索引、slot 数目的概念被应用于指向成员的指针概念。

表格驱动对象模型

为了所有classes所有objects有一致的表达方式,一种对象模型把members信息抽出来,放在一个成员变量和成员函数表格中。

1551323392870

C++对象模型

C++对象模型是从 简单对象模型派生出来的,并且对内存空间和存取时间做了优化。在这个模型中,非静态数据被配置与一个 class object 之内。静态数据成员、静态和非静态函数成员被被放在class 之外。而虚函数有两个步骤处理

  1. 每一个class产生一堆指向虚函数的指针,放在表格之中,这个表格称为 虚表
  2. 每一个class object 被添加了一个指针,指向相关的 虚表,通常这个指针被称为虚指针。虚指针的设定和重置都由class 的构造、析构和拷贝运算符自动完成。此外 class的 type_info 也被放在 虚表中

1551324026990

1551323883170

单例模式完全实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 线程不安全的单例
class Singleton{
private:
Singleton(){};
static Singleton* m_instance;
public:
static Singleton* getInstance(){
if(m_instance==NULL){
m_instance = new Singleton();
}
reutrn m_instance;
}
}

Singleton* Singleton::m_instance = NULL;

// 线程安全单例
// 懒汉模式
class Singleton{
private:
Singleton(){};
static Singleton* m_instance;
public:
static Singleton* getInstance(){
if(m_instance==NULL){
lock();
if(m_instance==NULL){
m_instance = new Singleton;
}
unlock();
}
return m_instance;
}
}
Singleton* Singleton::m_instance = NULL;

//饿汉模型
class Singleton{
private:
Singleton(){};
Static Singleton* m_instance;
public:
static Singleton* getInstance();
}
Singleton* Singleton::m_instance = new Singleton();
Singleton* Singleton::getInstance(){
reutrn m_instance;
}

C++11

1551326389293

智能指针的设计实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
 1 #include <iostream>
2 #include <memory>
3
4 template<typename T>
5 class SmartPointer {
6 private:
7 T* _ptr;
8 size_t* _count;
9 public:
10 SmartPointer(T* ptr = nullptr) :
11 _ptr(ptr) {
12 if (_ptr) {
13 _count = new size_t(1);
14 } else {
15 _count = new size_t(0);
16 }
17 }
18
19 SmartPointer(const SmartPointer& ptr) {
20 if (this != &ptr) {
21 this->_ptr = ptr._ptr;
22 this->_count = ptr._count;
23 (*this->_count)++;
24 }
25 }
26
27 SmartPointer& operator=(const SmartPointer& ptr) {
28 if (this->_ptr == ptr._ptr) {
29 return *this;
30 }
31
32 if (this->_ptr) {
33 (*this->_count)--;
34 if (this->_count == 0) {
35 delete this->_ptr;
36 delete this->_count;
37 }
38 }
39
40 this->_ptr = ptr._ptr;
41 this->_count = ptr._count;
42 (*this->_count)++;
43 return *this;
44 }
45
46 T& operator*() {
47 assert(this->_ptr == nullptr);
48 return *(this->_ptr);
49
50 }
51
52 T* operator->() {
53 assert(this->_ptr == nullptr);
54 return this->_ptr;
55 }
56
57 ~SmartPointer() {
58 (*this->_count)--;
59 if (*this->_count == 0) {
60 delete this->_ptr;
61 delete this->_count;
62 }
63 }
64
65 size_t use_count(){
66 return *this->_count;
67 }
68 };
69
70 int main() {
71 {
72 SmartPointer<int> sp(new int(10));
73 SmartPointer<int> sp2(sp);
74 SmartPointer<int> sp3(new int(20));
75 sp2 = sp3;
76 std::cout << sp.use_count() << std::endl;
77 std::cout << sp3.use_count() << std::endl;
78 }
79 //delete operator
80 }