C++源文件到可执行文件的过程
对于C/C++编写的程序,从源代码到可执行文件,一般经过下面四个步骤:
- 预处理,产生.ii文件
- 对所有的“#define”进行宏展开;
- 处理所有的条件编译指令,比如“#if”,“#ifdef”,“#elif”,“#else”,“#endif”
- 处理“#include”指令,这个过程是递归的,也就是说被包含的文件可能还包含其他文件
- 删除所有的注释“//”和“/**/”
- 添加行号和文件标识
- 保留所有的“#pragma”编译器指令
- 编译,产生汇编文件(.s文件)
编译的过程就是将预处理完的文件进行一系列词法分析,语法分析,语义分析及优化后生成相应的汇编代码文件(.s文件) - 汇编,产生目标文件(.o或.obj文件)
汇编器是将汇编代码转变成机器可以执行的代码(二进制文件),每一个汇编语句几乎都对应一条机器指令。最终产生目标文件(.o或.obj文件)。 - 链接,产生可执行文件(.out或.exe文件)
链接的过程主要包括了地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定位(Relocation)
Clang/LLVM/lldb/GCC/gdb
- Clang 是 LLVM 编译器工具集的前端(front-end),目的是输出代码对应的抽象语法树(Abstract Syntax Tree, AST),并将代码编译成LLVM Bitcode。接着在后端(back-end)使用LLVM编译成平台相关的机器语言 。Clang支持C、C++、Objective C。
- LLVM 提供了完整编译系统的中间层,它会将中间语言(Intermediate form,IF)从编译器取出与最优化,最优化后的 IF 接着被转换及链接到目标平台的汇编语言。LLVM 后端也可以接受来自GCC工具链所编译的 IF。
- lldb 是 LLVM 调试器(断点原理)。lldb是个开源的内置于XCode的具有REPL(read-eval-print-loop)特征的Debugger,其可以安装C++或者Python插件。
- GCC(GNU Compiler Collection)在所有平台上都使用同一个前端处理程序(支持很多语言),产生一样的中介码,因此此中介码在各个其他平台上使用GCC编译,有很大的机会可得到正确无误的输出程序。
- gdb 是 GCC 调试器。UNIX及UNIX-like下的调试工具。
ref Clang/LLVM/lldb/GCC/gdb 关系
头文件中的ifndef/define/endif有什么作用
这是C++预编译头文件保护符,保证即使文件被多次包含,头文件也只定义一次。
typedef 和 define 有什么区别
- 用法不同:typedef 用来定义一种数据类型的别名,增强程序的可读性。define 主要用来定义常量,以及书写复杂使用频繁的宏。
- 执行时间不同:typedef 是编译过程的一部分,有类型检查的功能。define 是宏定义,是预编译的部分,其发生在编译之前,只是简单的进行字符串的替换,不进行类型的检查。
- 作用域不同:typedef 有作用域限定。define 不受作用域约束,只要是在 define 声明后的引用都是正确的。
- 对指针的操作不同:typedef 和 define 定义的指针时有很大的区别。
注意:typedef 定义是语句,因为句尾要加上分号。而 define 不是语句,千万不能在句尾加分号。
#include 的顺序以及尖叫括号和双引号的区别
- #include的顺序的区别:
头文件的引用顺序对于程序的编译还是有一定影响的。如果要在文件a.h中声明一个在文件b.h中定义的变量,而不引用b.h。那么要在a.c文件中引用b.h文件,并且要先引用b.h,后引用a.h,否则汇报变量类型未声明错误,也就是常见的某行少个“;”符号。 - #include尖括号和双引号的区别:
- #include <> ,认为该头文件是标准头文件。编译器将会在预定义的位置集查找该头文件,这些预定义的位置可以通过设置查找路径环境变量或者通过命令行选项来修改。使用的查找方式因编译器的不同而差别迥异。
- #include “”,认为它是非系统头文件,非系统头文件的查找通常开始于源文件所在的路径。查找范围大于<>。
main 函数执行以前,还会执行什么代码?
全局对象的构造函数会在main 函数之前执行。
内联函数和普通函数的区别
- 复杂程度不同:
内联函数比较简单,在内联函数中不允许使用循环语句和switch结果,带有异常接口声明的函数也不能声明为内联函数。 - 编译结果不同:
内联函数(有时称作在线函数或编译时期展开函数)是一种编程语言结构,用来建议编译器对一些特殊函数进行内联扩展(有时称作在线扩展);也就是说建议编译器将指定的函数体插入并取代每一处调用该函数的地方(上下文)。普通函数则会编译为单独的模块。 - 编译的时间不同:
对于基于C的编译系统,内联函数的使用可能大大增加编译时间,因为每个调用该函数的地方都需要替换成函数体,代码量的增加也同时带来了潜在的编译时间的增加。 - 运行的效率不同:
使用内联函数时,必须在程序占用空间和程序执行效率之间进行权衡,因为过多的比较复杂的函数进行内联扩展将带来很大的存储资源开支。
内联函数和宏定义的区别
内联函数和宏的区别在于:
- 宏是由预处理器对宏进行替代
- 内联函数是通过编译器控制来实现的
而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。
内联函数与带参数的宏定义进行下比较,它们的代码效率是一样,但是内联函数要优于宏定义,因为内联函数遵循的类型和作用域规则,它与一般函数更相近,在一些编译器中,一旦关联上内联扩展,将与一般函数一样进行调用,比较方便。
另外,宏定义在使用时只是简单的文本替换,并没有做严格的参数检查,也就不能享受C++编译器严格类型检查的好处,另外它的返回值也不能被强制转换为可转换的合适的类型,这样,它的使用就存在着一系列的隐患和局限性。
C++的inline的提出就是为了完全取代宏定义,因为inline函数取消了宏定义的缺点,又很好地继承了宏定义的优点,《Effective C++》中就提到了尽量使用Inline替代宏定义的条款,足以说明inline的作用之大。
定义MAX和MIN宏
1 | #define MAX(a,b) ((a) > (b) ? (a) : (b)) |
大小端
大端:高位存在低地址,低位存在高地址。
小端:高位存在高地址,低位存在低地址。
大小端和CPU有关。
现代PC大多采用小段,所以小端字节序又被成为主机字节序。而大端字节序又被成为网络字节序。
判断大小端代码:
- 方法一
1 | void byteOrder(){ |
- 方法二
1 | void byteOrder(){ |
左值和右值
- 左值 (lvalue, locator value):表示了一个占据内存中某个可识别的位置(也就是一个地址)的对象。
- 右值 (rvalue):一个表达式不是 左值 就是 右值 。 那么,右值是一个 不 表示内存中某个可识别位置的对象的表达式。
左值引用 和 右值引用
C++11标准添加了右值引用(rvalue reference),这种引用只能绑定右值,不能绑定左值,它使用两个&&来声明:
1 | int a = 1; |
面向对象的三个基本特征
面向对象的三个基本特征是:封装、继承、多态。
封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类)。它们的目的都是代码重用;而多态则是为了实现另一个目的——接口重用。
封装
是指利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体,数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。
封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果不想被外界方法,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
不同的类成员访问修饰符权限:
访问修饰符 | 同一个类 | 同包 | 不同包,子类 | 不同包,非子类 |
---|---|---|---|---|
private | √ | |||
protected | √ | √ | √ | |
public | √ | √ | √ | √ |
默认 | √ | √ |
使用继承时需要注意:
1、子类拥有父类非private的属性和方法。
2、子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
3、子类可以用自己的方式实现父类的方法。
struct和class的区别
在C++中 struct和class唯一的区别就在于默认的继承访问权限不同。
- struct 默认权限为公共
- class 默认权限为私有
例子:
1 | class listNode{//链表类 |
构造函数和析构函数
构造函数和析构函数解决了对象的初始化和清理这两个非常重要的安全问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。
对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供构造函数和析构函数的空实现。
- 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
- 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法:类名(){}
- 没有返回值也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数语法: ~类名(){}
- 没有返回值也不写void
- 函数名称与类名相同,在名称前加上符号 ~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
- public、private、protected等权限控制对析构函数无效
构造函数与析构函数的特点
- 构造函数有如下特点:
- 构造函数的名字必须与类名相同;
- 构造函数可以有任意类型的参数,但不能有返回类型;
- 定义对象时,编译系统会自动调用构造函数;
- 构造函数是特殊的成员函数,函数体可以在类体内也可以在类体外;
- 构造函数被声明为公有函数,但它不能像其他成员函数那样被显式调用,它是在定义对象的同时被调用的。
- 析构函数有如下特点:
- 析构函数的名字必须与类名相同,但它前面必须加一个波浪号;
- 析构函数没有参数,也没有返回值,而且不能被重载,因此在一个类中只能有一个析构函数;
- 当撤销对象时,编译系统会自动调用析构函数;
- 析构函数可以是virtual,而构造函数不能是虚函数。
构造函数的分类及调用
两种分类方式:
- 按参数分为: 有参构造和无参构造
- 按类型分为: 普通构造和拷贝构造
ps. 有参构造有的可能会成为类型转换构造函数,比如在隐式转换调用的时候
三种调用方式:
- 括号法
- 显示法
- 隐式转换法
构造函数分类示例:
1 | // 按照参数分类分为 有参和无参构造 无参又称为默认构造函数 |
构造函数的调用示例:
1 | //调用无参构造函数 |
拷贝构造函数和赋值运算符的认识
拷贝构造函数和赋值运算符重载有以下两个不同之处:
- 拷贝构造函数生成新的类对象,而赋值运算符不能。
- 由于拷贝构造函数是直接构造一个新的类对象,所以在初始化这个对象之前不用检验源对象是否和新建对象相同。而赋值运算符则需要这个操作,另外赋值运算中如果原来的对象中有内存分配要先把内存释放掉
注意:当有类中有指针类型的成员变量时,一定要重写拷贝构造函数和赋值运算符,不要使用默认的。
构造函数和析构函数可以是虚函数吗?
构造函数不能是虚函数
- 存储空间角度
虚函数的调用需要 vptr 指针,而该指针存放在对象的内容空间中,需要调用构造函数才可以创建它的值,否则即使开辟了空间,该 vptr 指针为随机值;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有 vptr 地址用来调用虚函数之一的构造函数了。 - 使用,多态角度
虚函数主要是实现多态,在运行时才可以明确调用对象,根据传入的对象类型来调用函数,例如通过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过父类的指针或者引用去调用。那使用虚函数也没有实际意义。 - 从实现角度
vtable在构造函数调用后才建立,所以构造函数不能是虚函数。在调用构造函数时还不能确定对象的真实类型(由于子类会调父类的构造函数);并且构造函数的作用是提供初始化,在对象生命期仅仅运行一次,不是对象的动态行为,没有必要成为虚函数。
- 存储空间角度
析构函数常常是虚函数
创建一个对象时我们总是要明白指定对象的类型。虽然我们可能通过基类的指针或引用去访问它。但析构却不一定,我们往往通过基类的指针来销毁对象。这时候假设析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。
如果基类的析构函数不是虚函数,派生类的析构函数用不上,会造成资源的泄漏。
构造函数和析构函数,可以调用其他的虚函数吗?
《Effective C++》条款09:绝不在构造函数或析构函数中调用虚函数。
从语法上讲,调用完全没有问题。
但是从效果上看,往往不能达到需要的目的:派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。同样,进入基类析构函数时,对象也是基类类型。所以,虚函数始终仅仅调用基类的虚函数(如果是基类调用虚函数),不能达到多态的效果,所以放在构造函数中是没有意义的,而且往往不能达到本来想要的效果。
子类和父类中,构造函数和析构函数的调用顺序是什么?
- 构造时,先调用父类构造函数,再调用子类构造函数
- 析构时,先调用子类析构函数,再调用父类析构函数
ps.
- 若一个类包含对象成员,在建立该类的对象时,先调用对象成员的构造函数,初始化相应的对象成员,然后才执行该类的构造函数。
- 如果一个类包含多个对象成员,对象成员的构造函数的调用顺序由它们在该类中的说明顺序决定,而它们在初始化表中的顺序无关。
虚析构函数有什么作用?
- 析构函数的工作方式是:最底层的派生类的析构函数最先被调用,然后调用每一个基类的析构函数;
- 在C++中,当一个派生类对象通过使用一个基类指针删除,而这个基类有一个非虚的析构函数,则可能导致运行时派生类不能被销毁。然而基类部分很有可能已经被销毁,这就导致“部分析构”现象,造成内存泄漏;
- 给基类一个虚析构函数,删除一个派生类对象的时候就将销毁整个对象,包括父类和全部的派生类部分。
拷贝构造函数在什么情况下会自动被调用
- 当类的一个对象去初始化该类的另一个对象时;
- 如果函数的形参是类的对象,调用函数进行形参和实参结合时;
- 如果函数的返回值是类对象,函数调用完成返回时。
深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作。使用默认拷贝构造函数,拷贝过程中是按字节复制的,对于指针型成员变量只复制指针本身,而不复制指针所指向的目标,因此涉及堆区开辟内存时,会将两个成员属性指向相同的内存空间,从而在释放时导致内存空间被多次释放。
深拷贝:在堆区重新申请空间,进行拷贝操作。自定义拷贝构造函数,在堆内存中另外申请空间来储存数据,从而解决指针悬挂的问题。需要注意自定义析构函数中应该释放掉申请的内存。
如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的重复释放堆区问题。
拷贝构造函数的参数必须加const,因为防止修改,本来就是用现有的对象初始化新的对象。
ps. 在定义类或者结构体,这些结构的时候,最后都重写拷贝函数,避免浅拷贝这类不易发现但后果严重的错误产生
1 | //拷贝构造函数 |
类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员为对象成员。
当类中有对象成员时,构造的顺序是:先调用对象成员的构造,再调用本类构造。析构顺序与构造相反。
如:B类中有对象A作为成员,A为对象成员。那么当创建B对象时,先调用A的构造,再调用B的构造。结束时先析构B,再析构A。
静态成员
成员变量和成员函数前加上关键字static,称为静态成员。
静态成员变量:
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
静态成员函数:
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
静态成员两种访问方式:
- 通过对象。如:p1.m_A
- 通过类名。如:Person::m_B(私有权限访问不到)
成员变量和成员函数分开存储
- 非静态成员变量占对象空间。int mA;
- 静态成员变量不占对象空间。static int mB;
- 函数也不占对象空间,所有函数共享一个函数实例。每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码
1
2
3void func() {
cout << "mA:" << this->mA << endl;
} - 静态成员函数也不占对象空间
对象模型和this指针
this指针概念
在类实例化对象时,只有非静态成员变量属于对象本身,剩余的静态成员变量,静态函数,非静态函数都不属于对象本身,因此非静态成员函数只会实例一份,多个同类型对象会共用一块代码。c++通过提供特殊的对象指针——this指针——区分调用自己的对象。this指针指向被调用的成员函数所属的对象。
this指针的本质是一个指针常量,指针的指向不可修改。this的目的总是指向这个对象,
this 是一个指向类的实例的一个指针,指向该实例的首地址,但是 this 不是 该对象实例的一部分,即在sizeof(某对象)中不包含this指针的大小。
this指针的作用
this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行。在成员函数内部,我们可以直接使用调用该函数的对象的成员,而无需通过成员访问运算符来做到这一点,因为this所指的正是这个对象。任何对类成员的直接访问都被看成this的隐式使用。
- 当形参和成员变量同名时,可用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用
return *this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Person
{
public:
Person(int age)
{
//1、当形参和成员变量同名时,可用this指针来区分
this->age = age;
}
Person& PersonAddPerson(Person p)
{
this->age += p.age;
//返回对象本身
return *this;
}
int age;
};
this 指针需要注意的地方。
- this 指针只能用于成员函数,成员变量,对于静态函数和静态变量,是不允许使用this(因为静态函数或变量,都是属于对象本身,即所有实例都可以访问他们,但是this只是指向自身实例的地址,是一个个例。)
- 友元函数也没有this指针。(友元函数至少需要一个参数)
- this引用成员变量用法有二:
this->val
或者(*this).val.
这就像指针引用类似。
友元
友元的目的就是让一个函数或者类访问另一个类中私有成员(包括属性和方法),会破坏C++的封装性,尽量不使用。
友元的关键字为 friend
友元的三种实现:
- 全局函数做友元
- 类做友元
- 成员函数做友元
继承
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码,能够大大的提高开发的效率。
继承方式
继承方式一共有三种:
- 公共继承
- 保护继承
- 私有继承
继承中构造和析构顺序
继承中,先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
继承同名成员处理方式
当子类与父类出现同名的成员,通过子类对象访问子类或父类中同名的数据的方法:
- 访问子类同名成员:直接访问即可
- 访问父类同名成员:需要加作用域
ps. 同名静态成员处理方式和非静态处理方式一样
多继承
C++允许一个类继承多个类
语法:class子类:继承方式 父类1,继承方式 父类2...
ps. 多继承可能会引发父类中有同名成员出现,需要加作用域区分
多态
指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
多态性允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。
简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。
多态的实现原理
- 当类中存在虚函数时,编译器会在类中自动生成一个虚函数表
- 虚函数表是一个存储类成员函数指针的数据结构
- 虚函数表由编译器自动生成和维护
- virtual 修饰的成员函数会被编译器放入虚函数表中
- 存在虚函数时,编译器会为对象自动生成一个指向虚函数表的指针(通常称之为 vptr 指针)
多态实现的三个条件
- 要有继承
- 要有虚函数重写
- 要有父类指针指(父类引用)向子类对象
多态分为两类(实现多态的两种方法)
- 重载——静态多态:函数重载 和 运算符重载 属于静态多态,复用函数名
- 重写(覆盖)——动态多态: 派生类和虚函数实现运行时多态
静态多态和动态多态区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
其实,重载的概念并不属于“面向对象编程”,重载的实现是:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)。
如,有两个同名函数:function func(p:integer):integer;和function func(p:string):integer;。那么编译器做过修饰后的函数名称可能是这样的:int_func、str_func。对于这两个函数的调用,在编译器间就已经确定了,是静态的(记住:是静态)。
也就是说,它们的地址在编译期就绑定了(早绑定),因此,重载和多态无关!真正和多态相关的是“覆盖”。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态(记住:是动态!)的调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出)。因此,这样的函数地址是在运行期绑定的(晚邦定)。
结论就是:重载只是一种语言特性,与多态无关,与面向对象也无关。
动态多态代码示例
1 | #include <iostream> |
静态多态代码示例
1 | #include <iostream> |
虚函数
- 虚函数的作用主要是实现了多态的机制。
- 定义一个函数为虚函数,不代表函数为不被实现的函数。定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
- 定义一个函数为纯虚函数,才代表函数没有被实现。定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
- 虚函数的功能是使子类可以用同名的函数对父类函数进行覆盖,并且在通过父类指针调用时,如果有覆盖则自动调用子类覆盖函数,如果没有覆盖则调用父类中的函数,从而实现灵活扩展和多态性;
- 如果是纯虚函数,则纯粹是为了在子类覆盖时有个统一的命名而已,子类必须覆盖纯虚函数,则否子类也是抽象类;
- 含有纯虚函数的类称为抽象类,不能实例化对象,主要用作接口类。
示例:
1 | class A |
纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数。
当类中有了纯虚函数,这个类也称为抽象类
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0;
注意:
- 纯虚函数没有函数体
- 最后的
=0
并不是表示返回值为0,只是形式上的作用,告诉编译系统这是虚函数
- 这是一个声明,最后有分号
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
- 类中只要有一个纯虚函数就称为抽象类
虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:virtual ~类名(){}
纯虚析构语法:virtual ~类名() = 0;
类名::~类名(){}
1 | class Animal { |
虚基类和虚继承
多继承(Multiple Inheritance)是指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。尽管概念上非常简单,但是多个基类的相互交织可能会带来错综复杂的设计问题,命名冲突就是不可回避的一个。
多继承时很容易产生命名冲突,即使我们很小心地将所有类中的成员变量和成员函数都命名为不同的名字,命名冲突依然有可能发生,比如典型的是菱形继承。如:
1 | //间接基类A |
虚继承目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),继承的时候用关键字virtual
声明。
虚继承主要用来解决继承中的二义性问题:
1 | //间接基类A |
ref. C++虚继承和虚基类
虚函数表
虚函数表是指在每个包含虚函数的类中都存在着一个函数地址的数组。当我们用父类的指针来操作一个子类的时候,这张虚函数表指明了实际所应该调用的函数。
虚函数表指针vptr一般存储在对象实例的最开头,里面又虚函数表vtable的地址。虚函数表vtable在Linux/Unix中存放在可执行文件的只读数据段 .rodata 中
ref.
多态用法示例
1 | #include<iostream> |
符号
namespace
主要用来解决命名冲突的问题
- 必须在全局作用域下声明
- 命名空间下可以放函数,变量、结构体和类
- 命名空间可以嵌套命名空间
- 命名空间是开放的,可以随时加入新成员(添加时只需要再次声明namespace,然后添加新成员即可
::
(作用域运算符)
- 全局作用域符(::name):用于类型名称(类、类成员、成员函数、变量等)前,表示作用域为全局命名空间
- 类作用域符(class::name):用于表示指定类型的作用域范围是具体某个类的
- 命名空间作用域符(namespace::name):用于表示指定类型的作用域范围是具体某个命名空间的
using
- using声明
1
using std::cout;
- using编译指令
1
using namespace std;
ps. 尽量使用声明而不是编译指令,不同命名空间中可能会有相同的变量名,编译指令执行两个命名空间后,会产生二义性
ref
函数重载(overload)、重写(override,也叫覆盖)和隐藏(也叫重定义)
重载(overload)
当函数具有相同的名称,但是参数列表不相同的情形(包括参数的个数不同或参数的类型不同),这样的同名而不同参数的函数之间,互相被称之为重载函数。
(函数名相同,参数列表不同,overload只是在类的内部存在)
特征:
- 具有相同的作用域(即同一个类定义中);
- 函数名字相同;
- 函数参数 类型不同 或者 个数不同 或者 顺序不同;
- virtual 关键字可有可无;
- 返回类型也可以不同。
ps:函数的访问权限、返回类型、抛出的异常不可以作为函数重载的条件
函数重载实例判断:
1 | 以下的集中写法,分别表示了哪些是重载的,哪些不是重载的。 |
重写(覆盖,override)
重写(覆盖)是指派生类重新实现(或者改写)基类的成员函数,在继承关系之间。C++利用虚函数实现多态。其特征是:
- 不同的作用域(分别位于派生类和基类中);
- 完全相同的函数名,参数列表 和 返回类型;
- 基类函数必须是虚函数。即必须有virtual关键字,不能是static;
- 重写函数的访问修饰符可以不同。尽管父类的virtual方法是private的,派生类中重写改写为public、protected也是可以的;
重写(override)代码示例:B中fun1重写了A中的fun1
1 | class A{ |
隐藏(重定义)
子类重新定义父类有相同名称的非虚函数(参数列表可以不同)。
- 不在同一个作用域(分别位于派生类与基类) ;
- 函数名字相同,返回值可以不同;
- 函数名相同但参数不同。如果派生类的函数和基类的函数同名,但是参数不同,不论有无 virtual 关键字,基类的函数将被隐藏(注意别与重载以及覆盖混淆);
- 函数名相同且参数相同,但是基类函数没有 virtual关键字。此时,基类的函数被隐藏(否则就是重写了)。
示例程序:
1 | class Base |
- 重载:函数Base::A(int)与Base::A(float)相互重载
- 重写:函数Derived::B(void)重写了Base::B(void),函数Derived::C(float)重写了Base::C(float)
- 隐藏:函数Derived:: D(int)隐藏了Base:: D(float),函数Derived::E(float)隐藏了Base::E(float)
ref
- C++ 类成员函数的重载(overload),重写/覆盖(override),隐藏
- 函数重载(overload)和函数重写(override)
- C++的重载(overload)与重写(override)
函数模板与函数重载的异同?
- 函数的重载是指定义了几个名字相同,但参数的类型或参数的个数不同的函数;
- 模板函数是指的几个函数的具体算法相同,而参数类型不同的函数;
- 模板函数可以减少重载函数,但也可能引发错误。
C++中的空类,默认会产生哪些类成员函数
- 默认构造函数
- 析构函数
- 拷贝构造函数
- 赋值运算符(operator=)
- 取址运算符(operator&)(一对,一个非const的,一个const的)
1
2
3
4
5
6
7
8
9
10class Empty
{
public:
Empty(); // 缺省构造函数
Empty( const Empty& ); // 拷贝构造函数
~Empty(); // 析构函数
Empty& operator=( const Empty& ); // 赋值运算符
Empty* operator&(); // 取址运算符
const Empty* operator&() const; // 取址运算符 const
};
类在内存中的存储方式
- 类的静态成员变量编译时被分配到静态/全局区,因此静态成员变量是属于类的,所有对象共用一份,不计入类的内存空间
- 静态成员函数和非静态成员函数都是存放在代码区的,是属于类的,类可以直接调用静态成员函数,不可以直接调用非静态成员函数,两者主要的区别是有无this指针
- 派生类对象的存储空间 = 基类存储空间 + 派生类特有的非static数据成员的空间
C++类在内存中的存储方式
类成员变量的初始化顺序
成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。因为成员变量的初始化次序是根据变量在内存中次序有关,而内存中的排列顺序早在编译期就根据变量的定义次序决定了。
数据类型
sizeof 与 strlen 的区别
- sizeof是一个操作符,而strlen是库函数。
- sizeof的参数可以是数据的类型,也可以是变量,而strlen只能以结尾为’\0’的字符串作参数。
- 编译器在编译时就计算出了sizeof的结果,而strlen必须在运行时才能计算出来。
- sizeof计算数据类型占内存的大小,strlen计算字符串实际长度。
1
2cout << strlen("123") << endl; //3
cout << sizeof("123") << endl; //4
strcpy 和 memcpy 的区别
strcpy 和 memcpy 都是标准C库函数
strcpy
strcpy提供了字符串的复制。即strcpy只用于字符串复制,并且它不仅复制字符串内容之外,还会复制字符串的结束符。
char* strcpy(char* dest, const char* src);
1 | char * strcpy(char * dest, const char * src) // 实现src到dest的复制 |
memcpy
memcpy提供了一般内存的复制。即memcpy对于需要复制的内容没有限制,因此用途更广。
void *memcpy( void *dest, const void *src, size_t count );
1 | void *memcpy(void *memTo, const void *memFrom, size_t size) |
区别
- 复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
- 复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符”\0”才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
- 用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy
x & (x-1)
相当于消除了 x 从右向左数遇到的第一个1。
short i = 0; i = i + 1L;这两句有错吗
代码一是错的,代码二是正确的。
说明:在数据安全的情况下大类型的数据向小类型的数据转换一定要显示的强制类型转换。
&&和&、||和|有什么区别
- &和|对操作数进行求值运算,&&和||只是判断逻辑关系。
- &&和||在在判断左侧操作数就能确定结果的情况下就不再对右侧操作数求值。
注意:在编程的时候有些时候将&&或||替换成&或|没有出错,但是其逻辑是错误的,可能会导致不可预想的后果(比如当两个操作数一个是 1 另一个是 2 时。
struct定义的四种方法
第一种
1 | struct 结构体名称{ |
第二种
1 | typedef struct 结构体名称{ |
第三种
1 | struct 结构体名称{ |
第四种:此方式是匿名结构体,在定义时同时声明结构体变量,但不能在其它地方声明,因为我们无法得知该结构体的标识符,所以就无法通过标识符来声明变量。
1 | struct { |
联合(union)、结构(struct)、类(class)
联合
在一个“联合”内可以定义多种不同的数据类型, 一个被说明为该“联合”类型的变量中,允许装入该“联合”所定义的任何一种数据,这些数据共享同一段内存,以达到节省空间的目的。union变量所占用的内存长度等于最长的成员的内存长度。1
2
3
4
5
6union A
{//sizeof(union A)的值为8
char mark;
long num;
float score;
};结构
将不同类型的数据组合成一个整体,是自定义类型。1
2
3
4
5
6struct B
{//sizeof(struct B)的值为24
char mark;
long num;
float score;
};结构体:将不同类型的数据组合成一个整体,是自定义类型
区别:
- 结构体中的每个成员都有自己独立的地址,它们是同时存在的;共同体中的所有成员占用同一段内存,它们不能同时存在;
- sizeof(struct)是内存对齐后所有成员长度的总和,sizeof(union)是内存对齐后最长数据成员的长度
结构体为什么要内存对齐呢?
看下面
内存对齐(字节对齐)
#pragma pack(n)
表示的是设置n字节对齐,windows默认是8,linux是4。
内存对齐规则
对于结构的各个成员,第一个成员位于偏移为0的位置,以后的每个数据成员的偏移量必须是 min(#pragma pack()指定的数,这个数据成员的自身长度)的倍数。
在所有的数据成员完成各自对齐之后,结构或联合体本身也要进行对齐,对齐将按照 #pragam pack指定的数值和结构或者联合体最大数据成员长度中比较小的那个,也就是 min(#pragram pack() , 长度最长的数据成员)。
需要对齐的原因
- 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
- 硬件原因:经过内存对齐之后,CPU的内存访问速度大大提升。访问未对齐的内存,处理器要访问两次(数据先读高位,再读低位),访问对齐的内存,处理器只要访问一次,为了提高处理器读取数据的效率,我们使用内存对齐
举例
1 | struct A{ |
- char占一个字节,起始偏移为零,int占四个字节,min(8,4)=4;所以应该偏移量为4,所以应该在char后面加上三个字节,不存放任何东西,short占两个字节,min(8,2)=2;所以偏移量是2的倍数,而short偏移量是8,是2的倍数,所以无需添加任何字节,所以第一个规则对齐之后内存状态为0xxx|0000|00
- 此时一共占了10个字节,但是还有结构体本身的对齐,min(8,4)=4;所以总体应该是4的倍数,所以还需要添加两个字节在最后面,所以内存存储状态变为了 0xxx|0000|00xx,一共占据了12个字节
ref. C/C++内存对齐
c++资源管理机制
内存的分类
堆(heap):指的是动态分配内存的区域。这里的内存,需要程序员手动分配和释放(new,delete),否则,就会造成内存泄漏。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
与之相关的一个概念是自由存储区(free store),特指使用 new 和 delete 来分配和释放内存的区域。一般来说,这是堆的一个子集。
new 和 delete 操作的区域是 free store
malloc 和 free 操作的区域是 heap
ps: new 和 delete 通常底层使用 malloc 和 free 来实现,所以 free store 也属于 heap栈(stack):函数调用过程中产生的本地变量和调用数据的区域。由编译器自动分配和释放。
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
凡生命周期超出当前函数的,一般需要用堆(或者使用对象移动传递)。反之,生命周期在当前函数内的,就该用栈。
全局/静态存储区(static):全局变量和静态变量被分配到同一块内存中。程序结束后由系统释放。
它们是在程序编译、链接时完全确定下来的,具有固定的存储位置(暂不考虑某些系统的地址扰乱机制)。堆和栈上的变量则都是动态的,地址无法确定。常量存储区:这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。程序结束后由系统释放。
程序代码区:存放程序的二进制代码。
栈展开(stack unwinding)
指的是:如果在一个函数内部抛出异常,而此异常并未在该函数内部被捕捉,就将导致该函数的运行在抛出异常处结束,所有已经分配在栈上的局部变量都要被释放。
示例
最常见的栈展开就是正常的函数调用,任何一个函数返回都存在栈展开。C++引入异常机制后,当程序抛出异常,在异常向上传递的过程中,其函数调用栈也会展开。
1 | #include <stdio.h> |
代码执行结果:不管是否发生了异常,obj 的析构函数都会得到执行。
1 | Obj() |
RAII(Resource Acquisition Is Initialization)
是 C++ 所特有的资源管理方式。RAII 依托栈和析构函数,来对所有的资源(包括堆内存在内)进行管理。
其原理是在对象析构函数中释放该对象获取的资源,利用栈展开过程栈上对象的析构函数将被自动调用的保证,从而正确地释放先前获取的资源。
RAII只有在栈展开正常执行的前提下才能正常工作。函数调用和正常的C++异常处理流程(异常处于try-catch块)都存在栈展开。
申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
malloc/free 和 new/delete 区别:
属性不同:malloc/free是标准库函数,new/delete是操作符(运算符)。
申请的内存所在位置:new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。
内存分配失败时的返回值:new内存分配失败时,会抛出bad_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。
是否需要指定内存大小:new/delete分配可以自动计算需要的字节数,malloc/free需要人为指定。
是否调用构造函数/析构函数:
- new会先调用operator_new函数,申请足够的内存(通常底层使用malloc实现),然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator_delete函数释放内存(通常底层使用free实现)。
- malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
能否重载:new/delete允许重载,malloc/free不允许重载
已分配内存的扩充:malloc/free可以通过realloc函数扩充,new/free无法直观地处理
能否相互调用:operator_new/operator _delete的实现可以基于malloc/free,而malloc的实现不可以去调用new。
1
2
3
4
5
6
7
8
9
10
11
12
13
14//main.cpp
int a = 0; //全局初始化区
char *p1; //全局未初始化区
main()
{
int b; //栈
char s[] = "abc"; //栈
char *p2; //栈
char *p3 = "123456"; //123456\0在常量区,p3在栈上。
static int c =0; //全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20); //分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指的"123456"优化成一个地方
}
malloc实现原理
- malloc 函数的实质是它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。
- 调用 malloc()函数时,它沿着连接表寻找一个大到足以满足用户请求所需要的内存块。 然后,将该内存块一分为二(一块的大小与用户申请的大小相等,另一块的大小就是剩下来的字节)。 接下来,将分配给用户的那块内存存储区域传给用户,并将剩下的那块(如果有的话)返回到连接表上。
- 调用 free 函数时,它将用户释放的内存块连接到空闲链表上。
- 到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段, 那么空闲链表上可能没有可以满足用户要求的片段了。于是,malloc()函数请求延时,并开始在空闲链表上检查各内存片段,对它们进行内存整理,将相邻的小空闲块合并成较大的内存块。
malloc会出现的问题:初始化的问题。没有初始化的内存中内容是随机的,所以如果直接使用的话,就可能造成程序运行结果不正确。
ps. malloc 函数可以向程序的虚拟空间申请一块虚拟地址空间,与物理内存没有直接的关系,所以是有可能用malloc函数申请超过该机器物理内存大小的内存块的
ref. linux-malloc底层实现原理
delete 与 delete[] 区别
delete只会调用一次析构函数,而delete[]会调用每一个成员的析构函数。delete与new配套,delete []与new []配套。
在More Effective C++中有更为详细的解释:“当delete操作符用于数组时,它为每个数组元素调用析构函数,然后调用operator delete来释放内存。”
1 | MemTest *mTest1 = new MemTest[10]; |
说明:对于内建简单数据类型,delete和delete[]功能是相同的。对于自定义的复杂数据类型,delete和delete[]不能互用。
delete[]删除一个数组,delete删除一个指针。
简单来说,用new分配的内存用delete删除;用new[]分配的内存用delete[]删除。delete[]会调用数组元素的析构函数。内部数据类型没有析构函数,所以问题不大。如果你在用delete时没用括号,delete就会认为指向的是单个对象,否则,它就会认为指向的是一个数组。
内存泄漏
当一个对象已经不需要再使用本该被回收时,另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。内存泄漏会最终会导致内存溢出。
内存泄漏的原因:
- 异常或分支导致delete未得到执行
- 分配和释放不在一个函数里导致的遗漏delete
内存溢出
程序运行过程中申请的内存大于系统能够提供的内存,导致无法申请到足够的内存,于是就发生了内存溢出。
野指针
野指针指向一个已删除的对象 或 申请访问受限内存区域的指针。
原因:
- 指针变量未初始化。解决办法:指针声明时初始化,可以是具体的地址值,也可让它指向 NULL。
1
char *p; //此时p为野指针
- 指针释放未置空:指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。解决办法:指针指向的内存空间被释放后指针应该指向。NULL。
1
2
3char *p=new char[10]; //指向堆中分配的内存首地址
cin>> p;
delete []p; //p重新变为野指针 - 指针操作超出作用域。返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。解决办法:在变量的作用域结束前释放掉变量的地址空间并且让指针指向 NULL。ps. “野指针”的解决方法也是编程规范的基本原则,平时使用指针时一定要避免产生“野指针”,在使用指针前一定要检验指针的合法性。
1
2
3char *p=new char[10]; //指向堆中分配的内存首地址
cin>> p;
cout<<*(p+10); //可能输出未知数据
栈内存与文字常量区
1 | char str1[] = "abc"; |
str1,str2,str3,str4是数组变量,它们有各自的内存空间;而str5,str6,str7,str8是指针,它们指向相同的常量区域。
ref
- 极客时间《现代C++实战30讲》:01 | 堆、栈、RAII:C++里该如何管理资源?
- RAII(wiki)
- C++ 自由存储区是否等价于堆?
- 为什么c++中要分为heap(堆)和stack(栈)
- C/C++内存分配
- C/C++内存管理详解
指针和引用的区别
声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。不能建立数组的引用。
- 引用只是变量的一个别名,指针是变量的地址,有分配内存。
- 指针可以指向空值,但是在任何情况下都不能使用指向空值的引用。引用在声明时必须初始化。
- 指针与引用的另一个重要的不同是:指针可以被重新赋值以指向另一个不同的对象,但是引用则总是指向在初始化时被指定的对象,以后不能改变。
- sizeof的意义不同:使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小。
- 指针和引用的自增运算符意义不同:指针是对内存地址的自增,引用是对值的自增
- 没有引用常量,有指针常量
- 参数传递:作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象
- 多级指针,一级引用. 指针可以有多级指针(**p),而引用只有一级
- 作为参数时,引用更安全,因为指针传递时会涉及到形参和实参,会多开辟内存。
在以下情况下你应该使用指针:
- 你考虑到存在不指向任何对象的可能(在 这种情况下,你能够设置指针为空)
- 你需要能够在不同的时刻指向不同的对象(在这种情况下,你能改变指针的指向)。
如果总是指向一个对象并且一旦指向一个对象后就不会改变指向,那么你应该使用引用。
ps: 函数传参时,可以使用引用。
  引用也可以作为函数的返回值。但是要注意不要返回局部变量引用
。
1 | //返回局部变量引用 |
ref
- 《more effective C++》 Item M1:指针与引用的区别
- C++中指针和引用的区别
- C/C++基础知识
函数参数传递中值传递、地址传递、引用传递有什么区别?
- 值传递,会为形参重新分配内存空间,将实参的值拷贝给形参,形参的值不会影响实参的值,函数调用结束后形参被释放;
- 引用传递,不会为形参重新分配内存空间,形参只是实参的别名,形参的改变会影响实参的值,函数调用结束后形参不会被释放;
- 地址传递,形参为指针变量,将实参的地址传递给函数,可以在函数中改变实参的值,调用时为形参指针变量分配内存,结束时释放指针变量。
常量指针,指针常量,常量引用,没有引用常量
- 常量指针(常指针):是一个指针,指向一个常量。
- 指针常量:是一个常量,这个常量的类型是指针
- 常量引用:是一个引用,是常量的引用
- 没有引用常量
1 | //常量指针 |
常引用有什么作用
常引用的引入主要是为了避免使用变量的引用时,在不知情的情况下改变变量的值。
常引用主要用于定义一个普通变量的只读属性的别名、作为函数的传入形参,避免实参在调用函数中被意外的改变。
说明:很多情况下,需要用常引用做形参,被引用对象等效于常对象,不能在函数中改变实参的值,这样的好处是有较高的易读性和较小的出错率。
指针常量与常量指针区别
- 指针常量是指这个指针的值只能在定义时初始化,其他地方不能改变。(重点在常量)
- 常量指针是指这个指针指向一个只读的对象,不能通过常量指针来改变这个对象的值。(重点在指针)
指针常量强调的是指针的不可改变性,而常量指针强调的是指针对其所指对象的不可改变性。
注意:无论是指针常量还是常量指针,其最大的用途就是作为函数的形式参数,保证实参在被调用函数中的不可改变特性。
指针和数组
指针和数组的区别
- 概念不同。指针相当于一个变量,它存放的是数据在内存中的地址;数组是用于储存多个相同类型数据的集合
- 赋值不同。同类型指针变量可以相互赋值,数组不行,只能一个一个元素的赋值或拷贝
- 访问数据不同。指针是间接访问数据,获取指针,先解引用,再访问指针指向的地址中的内容;数组是直接访问
- sizeof意义不同。数组所占存储空间的内存:sizeof(数组名) 数组的大小:sizeof(数组名)/sizeof(数据类型) 在32位平台下,sizeof(指针名)是4,在64位平台下,sizeof(指针名)是8
- 指针和数组名异同。指针和数组名都可以表示地址,但指针是变量,可以修改;数组名是常量,不可修改赋值
- 传参。数组传参时会退化成指针
指针数组和数组指针
指针数组:它本质上是一个数组,数组的每个元素存放的是一个指针类型的元素。 int* arr[8];
- 优先级问题:[]的优先级比*高
- 说明arr是一个数组,而int*是数组里面的内容
- 这句话的意思就是:arr是一个含有8和int*的数组
数组指针:它本质上是一个指针,该指针指向一个数组。 int (*arr)[8];
- 由于[]的优先级比高,因此在写数组指针的时候必须将arr用括号括起来
- arr先和*结合,说明p是一个指针变量
- 这句话的意思就是:指针arr指向一个大小为8个整型的数组。
ps. 谁优先级高,本质是谁
数组名和指针的区别
数组名并不是真正意义上的指针,它的内涵要比指针丰富的多。但是当数组名当做参数传递给函数后,其失去原来的含义,变作普通的指针。另外要注意 sizeof 不是函数,只是操作符。
1 | #include <iostream.h> |
数组
数组的初始化
int a[10]和int* a = new int[10]的区别:
- int a[10]是静态分配
- int* a=new int[10]]是动态分配
数组的存放
- 固定数组
- 在函数体内分配的(不带static)是在栈中
- 全局变量/带static的局部变量 是在全局数据存储区
- 类中分配的在堆中
- 动态数组,都在堆中
说明:
int a[10]
使用简单,系统会自动实现内存的分配和回收。int* a = new int[10]
需要判断内存是否分配成功,以及在不用时需要使用delete[] a
进行内存释放,否则会造成内存泄漏;- 如果不是
a[10]
,而是a[1000000000]
或者更大的话,那一般情况下,就只能使用int* a = new
这种方式了。这个涉及到内存存放位置的问题,int a[]
这种方式,内存是存放在栈上;int* a = new
这种方式,内存是存放在堆上,栈的实际内存是连续内存,因此可分配空间较小,堆可以是非连续内存,因此可以分配较大内存。因此,如果需要分配较大内存,需要分配在堆上;(注意,同一个new出来的是连续内存,new一个一维数组确实是连续内存,但多个new出来的就不是连续内存了。) - 使用
int a[10]
这种方式,内存大小需要用常量指定,比如这里的10。不能用int m=10; int a[m]
这种方式。但是int* a= new
这种方式可以,因此在动态分配内存上,后者有非常大的优势。
ref
模版
C++另一种编程思想称为泛型编程,主要利用的技术就是模板。
C++提供两种模板机制:函数模板 和 类模板
函数模版
函数模板作用:建立一个通用函数,其函数返回值类型和形参类型可以不具体制定,用一个虚拟的类型来代表。
语法:
1 | template<typename T> |
- template:声明创建模板
- typename:表面其后面的符号是一种数据类型,可以用class代替
- T:通用的数据类型,名称可以替换,通常为大写字母
使用函数模板有两种方式:
- 自动类型推导
- 显示指定类型
示例
1 | template<typename T> |
普通函数与函数模板的区别
- 普通函数调用时可以发生自动类型转换(隐式类型转换)
- 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
- 如果利用显示指定类型的方式,可以发生隐式类型转换
普通函数与函数模板的调用规则
1 | //普通函数与函数模板调用规则 |
函数模版实例
转自下方参考1
利用函数模板封装一个排序的函数,可以对不同数据类型数组进行排序
排序规则从大到小,排序算法为选择排序
分别利用char数组和int数组进行测试
1 | //交换的函数模板 |
类模版
类模板作用:建立一个通用类,类中的成员 数据类型可以不具体制定,用一个虚拟的类型来代表。
语法:
1 | template<typename T> |
- template:声明创建模板
- typename:表面其后面的符号是一种数据类型,可以用class代替
- T:通用的数据类型,名称可以替换,通常为大写字母
示例
1 | template<class NameType, class AgeType> |
类模板与函数模板区别
1 | #include <string> |
ref
关键字
const
不可修改
- 修饰变量,说明该变量不可以被改变;
- 修饰指针,分为指向常量的指针和指针常量;int *const p和const int *p
- 常量引用,经常用于形参类型,即避免了拷贝,又避免了函数对值的修改;
- 修饰成员函数,说明该成员函数内不能修改成员变量,本质是const this指针。
static
对外不可见
- 修饰普通变量,修改变量的存储区域和生命周期,使变量存储在静态区,在 main 函数运行前就分配了空间,在整个程序运行期间一直存在,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它,自动初始化为0。
- 全局变量作用域:全局静态变量在声明他的文件之外是不可见的,即便是 extern 外部声明也不可以。准确地说是从定义之处开始,到文件结尾。
- 局部变量作用域:仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;
- 修饰普通函数,其只能在定义它的源文件中使用,不能在其他源文件中被引用
- 修饰类成员变量和成员函数,它们是属于类的,而不是某个对象,所有对象共享一个静态成员。静态成员通过<类名>::<静态成员>来使用。在 static 函数内不能访问非静态成员
extern
修饰变量或函数,表示该函数可以跨文件访问,或者表明该变量在其他文件定义,在此处引用。
extern关键字的作用是共享代码。
- 在其他文件中定义过的全局变量,在另一个文件中要调用时,只需在声明语句前加关键字extern。
- 对于常量,要调用其他文件的常量时,做法如下:
1
2
3
4
5//文件1,定义常量
extern const int i = 1;
//文件2,声明常量
extern const int i;
注意:
- 定义也是声明,因为当定义变量时我们也向程序表明了它的类型和名字
- 但声明不是定义,可以通过使用extern关键字声明变量而不定义它。不定义变量的声明包括对象名、对象类型和对象类型前的关键字extern
- extern声明不是定义,不分配存储空间
ref.
extern “C”
1 | #ifdef _cplusplus |
extern “C”的作用是,告诉C++编译器,下面的代码按照C的方式进行编译,不要对这些函数进行名字重整(function name mangling)。通常在C++程序中使用C函数或者模块时,需要用到这个功能。
ref.
explicit
explicit关键字的作用就是防止对象间实现使用 “=” 赋值,防止类构造函数的隐式自动转换,类构造函数默认情况下即声明为implicit(隐式),另外explicit只用于单参数的构造函数,或者除了第一个参数以外的其他参数都有默认值.
- explicit 修饰构造函数时,可以防止隐式转换和复制初始化
- explicit 修饰转换函数时,可以防止隐式转换
inline
用于程序中定义内联函数。
内联函数是C++中的一种特殊函数,它可以像普通函数一样被调用,但是在调用时并不通过函数调用的机制而是通过将函数体直接插入调用处来实现的,这样可以大大减少由函数调用带来的开销,从而提高程序的运行效率。一般来说inline用于定义类的成员函数。
在类定义中的定义的函数都是内联函数,即使没有使用 inline 说明符。类内声明可以不用加上inline关键字,但是类外定义函数体时必须要加上,这样才能保证编译器能够识别其为内联函数。
ps. 内联函数不能包括复杂的控制语句,如循环语句和switch语句
示例:使用内联函数来返回两个数中的最大值
1 | inline int Max(int x, int y) |
restrict
restrict只能修饰指针,restrict修饰的指针是能够访问所指区域的唯一入口,限制多个指针指向同一地址。
volatile
volatile是给编译器的指示来说明对它所修饰的对象不应该执行优化。volatile的作用就是用来进行多线程编程。在单线程中那就是只能起到限制编译器优化的作用。
如果一个基本变量被volatile修饰,编译器将不会把它保存到寄存器中,而是每一次都去访问内存中实际保存该变量的位置上。这一点就避免了没有volatile修饰的变量在多线程的读写中所产生的由于编译器优化所导致的灾难性问题。所以多线程中必须要共享的基本变量一定要加上volatile修饰符。
强制类型转换
C++中4种类型转换为:
- static_cast
完成基础数据类型;同一个继承体系中类型的转换;任意类型与空指针类型void* 之间的转换,不能用于普通指针的转换(void空指针除外) - dynamic_cast
动态类型转换,用于实现RTTI(运行时类型检查)。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常bad_cast - const_cast
用于删除 const、volatile特性 - reinterpret_cast
几乎什么都可以转,不能丢掉 const、volatile特性
多重继承
多重继承(多继承,Multiple Inheritance,MI)指的是一个类可以同时继承多个类,比如A类继承自B类和C类,这就是多重继承。
ref
- Effective C++ 40:明智地使用多继承
变量的声明和定义有什么区别
为变量分配地址和存储空间的称为定义,不分配地址的称为声明。一个变量可以在多个地方声明,但是只在一个地方定义。加入 extern
修饰的是变量的声明,说明此变量将在文件以外或在文件后面部分定义。说明:很多时候一个变量,只是声明不分配内存空间,直到具体使用时才初始化,分配内存空间,如外部变量。
局部变量,静态局部变量,全局变量,静态全局变量的区别
静态局部变量
- 该变量在全局数据区分配内存;
- 静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化;
- 静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为 0;
- 它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束。
静态全局变量
- 静态变量都在全局数据区分配内存;
- 未经初始化的静态全局变量会被程序自动初始化为0(在函数体内声明的自动变量的值是随机的,除非它被显式初始化,而在函数体外被声明的自动变量也会被初始化为 0);
- 静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的。
全局变量 和 静态全局变量 的区别
- 全局变量是不显式用 static 修饰的全局变量,全局变量默认是有外部链接性的,作用域是整个工程,在一个文件内定义的全局变量,在另一个文件中,通过 extern 全局变量名的声明,就可以使用全局变量。
- 静态全局变量是显式用 static 修饰的全局变量,作用域是声明此变量所在的文件,其他的文件即使用 extern 声明也不能使用。
存放区别
全局(静态)存储区:分为 DATA 段和 BSS 段。DATA 段(全局初始化区)存放初始化的全局变量和静态变量;BSS 段(全局未初始化区)存放未初始化的全局变量和静态变量。程序运行结束时自动释放。其中BBS段在程序执行之前会被系统自动清0,所以未初始化的全局变量和静态变量在程序执行之前已经为0。
全局初始化的变量:.data:
全局未初始化变量:.bss
全局只读:.rdata
ref
- C/C++ 中 static 的用法全局变量与局部变量
- 局部变量,静态局部变量,全局变量,静态全局变量在内存中的存放区别
- 全局初始化变量/全局未初始化变量/全局静态变量/局部变量的存储位置,作用域,与生命周期
C++中哪些运算符不可以重载?
- .
- ?:
- sizeof
- ::
- *
简述C++异常处理方式
一个典型的C++异常处理包含以下几个步骤:
- 程序执行时发生错误;
- 以一个异常对象(最简单是一个整数)记录错误的原因及相关信息;
- 程序监测到这个错误(读取异常对象);
- 程序决定如何处理错误;
- 进行错误处理,并在此后恢复/终止程序的执行。
STL(Standard Template Library,标准模板库)
STL从广义上分为: 容器(container)、算法(algorithm)、迭代器(iterator)
容器和算法之间通过迭代器进行无缝连接。STL几乎所有的代码都采用了模板类或者模板函数。
STL大体分为六大组件,分别是:
- 容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据。
- 算法:各种常用的算法,如sort、find、copy、for_each等
- 迭代器:扮演了容器与算法之间的胶合剂。
- 仿函数:行为类似函数,可作为算法的某种策略。
- 适配器:一种用来修饰容器或者仿函数或迭代器接口的东西。
- 空间配置器:负责空间的配置与管理。
常用STL容器、算法、迭代器
函数对象
重载函数调用操作符的类,其对象常称为函数对象
仿函数
函数对象使用重载的()时,行为类似函数调用,也叫仿函数。函数对象(仿函数)是一个类,不是一个函数。
例如:
1 | #include <iostream> |
函数对象的特点
- 函数对象在使用时,可以像普通函数那样调用, 可以有参数,可以有返回值
- 函数对象超出普通函数的概念,函数对象可以有自己的状态
- 函数对象可以作为参数传递
谓词
返回bool类型的仿函数称为谓词。如果operator()接受一个参数,那么叫做一元谓词;如果operator()接受两个参数,那么叫做二元谓词。
- 一元谓词
1
2
3
4
5
6
7struct GreaterFive{
bool operator()(int val) {
return val > 5;
}
};
vector<int>::iterator it = find_if(v.begin(), v.end(), GreaterFive()); - 二元谓词
1
2
3
4
5
6
7
8
9
10
11
12class MyCompare
{
public:
bool operator()(int num1, int num2)
{
return num1 > num2;
}
};
sort(v.begin(), v.end());//默认从小到大
sort(v.begin(), v.end(), MyCompare());//使用函数对象改变算法策略,排序从大到小
STL容器的底层数据结构
vector
- 底层使用数组保存。
- push_back时若已经满了,则会2*n扩展空间,若实际元素数量低于分配空间的1/4,则会将空间回收为原来的一半。
- 扩容时,先申请新的空间,然后将旧空间的内容拷贝过去,然后再释放旧的空间。
- 只适用于快速查找及只在末尾增删,而不适用于动态增删(可能涉及到元素的移动)。对元素进行增删时,可能导致旧的迭代器失效。防止迭代器失效的删除方法:
1
2
3
4
5
6
7for (vector<int>::iterator it = vec.begin();ite!=vec.end();)
{
if(*it % 2 != 0) //删除vec中的奇数
it = vec.erase(it);
else
it++;
} - clear()可以清空所有元素,但是即使clear(),vector所占用的内存空间依然如故,无法保证内存的回收。可以用swap()来帮助你释放内存,也可以使用erase循环删除第一个。
- vector和数组的区别
list
- 底层是双向链表,支持头尾增删,并且是一个环。
- 不适用于查找频繁的情况,但适用于动态增删。
stack
- 适配器。默认底层使用deque,适配之后只能从头插入和删除。
queue
- 适配器。默认底层使用deque,适配之后只能从尾插入,从头删除。
priority_queue
- 适配器。一般以vector为底层容器,堆heap为处理规则来管理。
map、multimap、set、multiset
- 底层使用红黑树实现,multimap是key值可重复的map。
- 防止迭代器失效的删除方法
1
2
3
4
5
6for (auto it = m.begin(); it != m.end();){
if(it->second == 10)//删除val==10的元素
m.erase(it++);
else
it++;
}
hash_map、hash_multimap、hash_set、hash_multiset
- 底层使用hashtable实现,其中hashtable是采用开链法来防止哈希冲突的。
deque:
- 底层是一个分段的线性表。笼统的说就是使用了一个二维指针,第一维是每段的信息,而第二维就是一个数组了,实际保存的元素就是在这里。
- 头尾都支持插入,但是维护麻烦很多。
slist
- 使用单向链表实现的列表。
ps. queue,priority_queue,stack不是容器,是适配器,是对容器的再封装,没有迭代器
vector和list的异同
- 数据结构上的不同
- vector是用连续数组存储,内存空间连续,随机访问O(1)。内存不足是扩容一倍,申请更大的内存。
- list底层是双向链表,不需要连续内存。插入删除O(1),查找O(n)。
- 迭代器
- vector中iterator支持”+”,”+=”,”<”等操作,list中不支持。
push_back 和 emplace_back
C++11中,针对顺序容器(如vector、deque、list),新标准引入了三个新成员:emplace_front、emplace和emplace_back,这些操作构造而不是拷贝元素。这些操作分别对应push_front、insert和push_back,允许我们将元素放置在容器头部、一个指定位置之前或容器尾部。
当调用push或insert成员函数时,我们将元素类型的对象传递给它们,这些对象被拷贝到容器中。而当我们调用一个emplace成员函数时,则是将参数传递给元素类型的构造函数。emplace成员使用这些参数在容器管理的内存空间中直接构造元素。
push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。
map和unordered_map的区别
- 排序:map在缺省下,map按照递增的顺序进行排序;unordered_map不排序
- 内部原理:map内部采用了红黑树(自平衡的二叉搜索树),实现了数据排序;unordered_map内部采用了哈希表
- 搜索操作时间:map的搜索时间复杂度为O(log(n));unordered_map平均搜索时间O(1),最坏情况为O(n)
- 插入操作时间:map复杂度为log(n)+再平衡时间;unordered_map平均插入时间O(1),最坏情况为O(n)
- 删除操作时间:与插入操作时间复杂度一样
ref
- C++提高编程:后半部分有STL库的常用方法
- C/C++ 最常见50道面试题
- C/C++ 经典面试题(一)之常考概念
- 常见C++笔试面试题整理
C++ 11 新特性
- 关键字及新语法:auto、nullptr、for
- STL容器:std::array、std::forward_list、std::unordered_map、std::unordered_set
- 多线程:std::thread、std::atomic、std::condition_variable
- 智能指针内存管理:std::shared_ptr、std::weak_ptr、std::unique_ptr
- 其他:std::function、std::bind和lamda表达式
- C++11中对类(class)新增的特性:
- default/delete 控制默认函数
- override /final 强制重写/禁止重写虚函数
- 委托构造函数 Delegating constructors
- 继承的构造函数 Inheriting constructors
- 类内部成员的初始化 Non-static data member initializers
- 移动构造和移动赋值
nullptr常量
C++中NULL仅仅是define NULL 0的一个宏定义,因此,有时候会产生歧义。
- 比如f(char*)和f(int),参数传NULL的话到底该调用哪个?事实上,在VS下测试这样的函数重载会优先调用f(int),但是f(char *)也是正确的,因此C++引入nullptr来避免这个问题
- nullptr是一个空指针,可以被转换成其他任意指针的类型
auto关键字
让编译器替我们去分析表达式所属的类型,直接推导。尤其是STL中map的迭代器这种很长的类型,适合用auto。
decltype操作符
从表达式的类型推断出要定义的变量的类型,跟表达式的类型也就是参数类型紧密相关
- delctype (f()) sum = x; 并不实际调用函数f(),只是使用f()的返回值当做sum的类型
- delctype (i) sum = x;和delctype ((i)) sum = x; 其中i为int类型,前面的为int类型,后面的为int&引用
范围for语句
多与auto配合使用。如 for(auto n:nums)
多线程互斥锁
pthread_mutex_t
1 | //互斥量的创建 |
std::mutex
.lock()
、.unlock()
、lock_guard
、unique_lock
1 | #include<mutex> |
双层vector
vector<vector
lambda表达式
用于实现匿名函数,匿名函数只有函数体,没有函数名。
用法:[capture](parameters)->return-type {body}
[]
叫做捕获说明符,表示一个lambda表达式的开始。接下来()
是参数列表,即这个匿名的lambda函数的参数,->return-type
表示返回类型,如果没有返回类型,则可以省略这部分。最后{}
就是函数体部分了。
lambda函数能够捕获lambda函数外的具有自动存储时期的变量。函数体与这些变量的集合合起来叫闭包。
- [] 不截取任何变量
- [&} 截取外部作用域中所有变量,并作为引用在函数体中使用
- [=] 截取外部作用域中所有变量,并拷贝一份在函数体中使用
- [=, &foo] 截取外部作用域中所有变量,并拷贝一份在函数体中使用,但是对foo变量使用引用
- [bar] 截取bar变量并且拷贝一份在函数体重使用,同时不截取其他变量
- [x, &y] x按值传递,y按引用传递
- [this] 截取当前类中的this指针。如果已经使用了&或者=就默认添加此选项。
示例:
1 | auto func = [] () { cout << "hello,world"; }; |
智能指针
放弃了C++98提供了第一个智能指针:auto_ptr
。增加了3个新的智能指针:
- shared_ptr
- weak_ptr
- unique_ptr
智能指针本质上是一个类,它将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。智能指针的出现实际上就是为了可以方便的控制对象的生命期,是 RAII 资源管理功能的自然展现。
STL一共给我们提供了四种智能指针:
- auto_ptr
C++98提供的解决方案,C+11已将将其摒弃。原因是避免潜在的内存崩溃问题:将一个auto_ptr赋值给另一个auto_ptr时,程序将试图删除同一个对象两次。 - unique_ptr
当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做。 - shared_ptr
采用引用计数的策略 - weak_ptr
对于引用计数法实现的计数,总是避免不了循环引用(或环形引用)的问题,shared_ptr也不例外。为了解决类似这样的问题,C++11引入了weak_ptr,来打破这种循环引用。
weak_ptr是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是将一个shared_ptr赋值给weak_ptr不会改变shared_ptr的引用计数。不论是否有weak_ptr指向,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。
ps:将一个智能指针赋值给另一个智能指针时有多种方法:
1)定义陚值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本,缺点是浪费空间,所以智能指针都未采用此方案。
2)建立所有权(ownership)概念。对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的构造函数会删除该对象。然后让赋值操作转让所有权。这就是用于auto_ptr和uniqiie_ptr 的策略,但unique_ptr的策略更严格。
3)创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数。例如,赋值时,计数将加1,而指针过期时,计数将减1,。当减为0时才调用delete。这是shared_ptr采用的策略。
ref
C++中常用的设计模式
共有23种设计模式,但真正在开发中常用的模式有:
Factory Method(工厂模式);
Strategy(策略模式);
Singleton(单例模式);
- C++ 单例模式总结与剖析
- 懒汉模式:不到万不得已就不会去实例化类,也就是说在第一次用到类实例的时候才会去实例化:
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//普通懒汉
class Singleton
{
private:
Singleton(){
cout << "constructor called" << endl;
}
Singleton(Singleton&) = delete;
Singleton &operator=(const Singleton&) = delete;
static Singleton *instance_ptr;
public:
~Singleton(){
std::cout<<"destructor called"<<std::endl;
}
static Singleton* get_instance(){
if(instance_ptr==nullptr){
instance_ptr = new Singleton;
}
return instance_ptr;
}
};
Singleton* Singleton::instance_ptr = nullptr;
int main(){
Singleton* instance = Singleton::get_instance();
Singleton* instance2 = Singleton::get_instance();
return 0;
}
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//线程安全、内存安全的懒汉式单例(智能指针,锁)
class Singleton{
public:
typedef shared_ptr<Singleton> Ptr;
~Singleton(){
cout << "destructor" << endl;
}
static Ptr get_instance(){
if(instance_ptr==nullptr){// "double checked lock"
std::lock_guard<mutex> lk(m_mutex);
if (instance_ptr == nullptr)
instance_ptr = Ptr(new Singleton);
}
return instance_ptr;
}
private:
Singleton()
{
cout << "constructor" << endl;
}
Singleton(Singleton &) = delete;
Singleton &operator=(const Singleton &) = delete;
static Ptr instance_ptr;
static mutex m_mutex;//锁
};
// initialization static variables out of class
Singleton::Ptr Singleton::instance_ptr = nullptr;
mutex Singleton::m_mutex;
int main(){
Singleton::Ptr instance = Singleton::get_instance();
Singleton::Ptr instance1 = Singleton::get_instance();
return 0;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24//局部静态变量懒汉,最推荐
class Singleton{
public:
~Singleton(){
cout << "destructor" << endl;
}
static Singleton& get_instance(){//返回指针而不是返回引用无法避免用户使用 delete instance 导致对象被提前销毁。
static Singleton instance;
return instance;
}
private:
Singleton(){
cout << "constructor" << endl;
}
Singleton(Singleton &) = delete;
Singleton &operator=(const Singleton &) = delete;
};
int main(){
Singleton& instance = Singleton::get_instance();
Singleton& instance1 = Singleton::get_instance();
return 0;
}- 饿汉:在单例类定义的时候就进行实例化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class Singleton{
public:
~Singleton(){
cout << "destructor" << endl;
}
static Singleton* get_instance(){
return instance_ptr;
}
private:
static Singleton* instance_ptr;
Singleton(){
cout << "constructor" << endl;
}
Singleton(Singleton &) = delete;
Singleton &operator=(const Singleton &) = delete;
};
Singleton *Singleton::instance_ptr = new Singleton();
int main(){
Singleton* instance = Singleton::get_instance();
Singleton* instance1 = Singleton::get_instance();
return 0;
}
Iterator(迭代器模式);
Abstract Factory(抽象工厂模式);
Builder(建造者模式);
Adapter(适配器模式);
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
41class Target { // Target,客户期望的接口,可以使具体或抽象的类,也可以是接口
public:
virtual void Request() = 0;
virtual ~Target(){};
};
class Adaptee { //需适配的类
public:
void SpecificRequest() { cout << "Adaptee" << endl; }
};
class Adapter1 : public Target { //通过内部包装一个Adaptee对象,把源接口转换为目标接口:
private:
Adaptee* adaptee;
public:
Adapter1() { adaptee = new Adaptee(); }
void Request() { adaptee->SpecificRequest(); } // 调用Request()方法会转换成调用adaptee.SpecificRequest()
~Adapter1() { delete adaptee; }
};
class Adapter2 : public Target{
private:
Adaptee *adaptee;
public:
Adapter2() { adaptee = new Adaptee(); }
void Request() { adaptee->SpecificRequest(); }
~Adapter2() { delete adaptee; }
};
int main()
{
Target *target = new Adapter1();
target->Request();
delete target;
Target *target2 = new Adapter2();
target->Request();
delete target2;
return 0;
}Bridge(桥接模式);
Composite(组合模式);
Interpreter(解释器模式);
Command(命令模式);
Mediator(中介者模式);
Observer(观察者模式);
State(状态模式);
Proxy(代理模式)。
设计模式6大原则
- 单一职责原则(Single Responsibility Principle)
- 开放封闭原则(Open Close Principle)
- 里氏替换原则(Liskov Substitution Principle)
- 依赖倒置原则(Dependence Inversion Principle)
- 接口隔离原则(InterfaceSegregation Principles)
- 迪米特原则(Law of Demeter)也称最少知识原则
对编程规范的理解或认识
编程规范可总结为:
- 可行性
- 可读性
- 可移植性
- 可测试性