本文最后更新于2 分钟前,文中所描述的信息可能已发生改变。
目录
基础部分
面向对象(OOP)的特性
- 封装 把客观事务封装成抽象的类且可以控制类中数据的访问权限,拥有属性与方法,类和结构体就是封装的体现。
- 继承 子类继承父类的公有属性和函数。
- 多态 分为静态多态和动态多态,静态多态体现为类中函数的重载,在编译时期完成;动态多态体现为可以用父类的指针或引用指向子类对象,程序运行时会根据子类的实际类型调用其对应的方法。通过虚指针实现,含义虚函数的类会有一张虚表(编译时生成存放虚函数的入口地址,该类的实例化对象共用一张虚表),并且在实例化的时候会生成一个虚指针(保存虚表的地址),因此通过这个虚指针父类可以找到子类中的虚函数。
虚函数与虚指针
- 虚函数: 类中使用 virtual 修饰的函数,其实现可以留给子类实现,类似 Java 中的抽象方法但有不同,子类可以不重写父类的虚函数。定义虚函数的作用就是为了让子类可以重写从而使得编译器能够使用动态绑定来达到多态。含有虚函数的类其析构函数必须定义为虚函数,原因见下文。注意:普通函数、静态函数、构造函数(因为虚指针是调用完构造方法后生成的)不能是虚函数。
- 虚指针: 含有虚函数的类在实例化时会生成一个虚指针,该指针指向虚表。
虚函数不占用类的存储空间,虚指针占用类的存储空间,大小为一个指针的大小。
纯虚函数
不能提供实现,方法的实现必须需要在子类中完成,含有纯虚函数的类叫做抽象类,不能被实例化,其子类必须实现其纯虚函数才能实例化。这个纯虚函数类似接口。与JAVA中的定义类似。例如:virtual int test()=0;
内联函数inline
主要目的是为了减少函数调用的开销。即将定义为内联函数的函数体代码复制到所有调用该函数的地方,有点类似宏定义,但是比宏定义多了类型检查,也是在编译时期执行。省去了参数压栈、栈帧开辟与回收,返回结果生成临时副本等提高了程序运行速度。在类中定义的函数(虚函数除外)都会隐式变成内联函数,但实际会不会执行代码展开还是由编译器决定,一般比较复杂的比如含义循环、递归等复杂的语句的函数编译器不会将其当成内联函数处理(声明了也没用),也与函数的调用频率有关。一般用于简单的函数。缺点是会导致代码膨胀。
析构函数
类中用于正确释放其资源的函数,对象生命周期结束时编译器会自动调用且一个类中只能定义一个析构函数并且不能有参数,不显式声明会有默认的一个。虚析构函数可以是内联函数,但是在表现出多态的时候不可以是内联的,因为编译器需要在编译阶段确定调用的析构函数具体是哪个类的,只有在编译器具有实际的对象而不是对象的指针或引用时才会发生。例如:
class A{
public:
int* a;
public:
//构造函数
A(int* a_):a(a_){}
//析构函数
~A(){
delete a;
a=nullptr;
}
};
类模版和成员模版
相当于 Java 中的泛型。
class A{
template <class T>
T get(T i){
return i;
}
};
template <class T>
class B{
T i;
T get(T j){
return i+j;
}
};
template <class T>
class C{
T i;
template <typename U1,typename U2>
void print(U1 j,U2 k){
std::cout<<j<<k;
}
};
内存分配和管理(malloc、free、new、delete)
malloc 与 free 是 c 中的库函数;而 new 与 delete 则是 c++ 中的关键字。
malloc
:申请指定空间字节的内存,内存中的初值不确定,参数为要申请的内存大小,申请成功则返回一个 void* 类型的指针(类型擦除的,所以需要进行强制类型转换),若申请不成功为 null。使用例:int* i = (int*)malloc(sizeof(obj));
( obj 为 int 类型的变量)。free
: 释放指定对象的内存。new
: 其底层是先调用 malloc 再调用对象的构造函数,其会自动计算要分配内存的大小。delete
: 底层是先调用析构函数再调用 free。
拷贝构造函数(浅拷贝)
可用于通过一个已有类实例来初始化一个新的类实例,新类实例的属性值与被拷贝的类一样,个人认为是 C++ 中都是值传递,比如说给一个参数是对象类型的,函数传参的时候首先会先产生一个临时变量 temp (即参数变量),然后通过拷贝构造函数用实参来初始化这个相同类型的临时变量 temp ,拷贝完后析构掉这样临时变量 temp 。一般会在用类的一个的实例去初始化另一个类的实例、传递类类型的形参(防止悬空指针的出现)、返回类类型的返回值的情况下会使用到拷贝构造函数。没有显式声明则编译器会默认生成一个(不会处理静态成员变量)。含有指针类型的成员变量或动态分配内存的类必须显式声明一个拷贝构造函数。
class A{
private:
int a;
public:
A(int a_):a(a_){}
//跟普通构造函数一样,但是参数为引用,const可加可不加。
A(const A& classA){
//即用一个类去实例化另一个同类型的类,成员变量都要带上
a = classA.a;
}
};
const
修饰符
可以修饰变量、指针、引用、成员函数,修饰成员函数时不能与static
一起使用因为没有this
指针。
修饰变量 表示该变量不能再被重新赋值,即是个常量。
修饰指针
const int* ptr = &a;
表示一个常量指针,即指针指向的是一个常量,指针指向可以改变但改变后必须还是指向一个常量。(可以这么来理解,const右边修饰指针)int* const ptr = &a;
表示一个指针常量,即指针的指向不可以发生改变,但指向的对象的内容可以改变。(const
右边修饰一个变量)
修饰引用
const int& ref = &a;
表示一个常量引用,即这个引用的值不能被修改,引用的是常量。不存在引用常量这种,因为引用的实现是一个指针常量,在初始化之后就不能重新赋值了。
修饰成员函数
- 表示保证不会在此成员函数中修改成员变量的值(因为
this
被修饰为了const className* const this
,即既不能对this
赋值也不能改变this
指向对象的内容)但是该成员变量被mutable
修饰的可以在此中被修改,const
修饰的函数会被重载,但是只能被const
修饰的类的实例调用。例如:
C++class A{ public: int a; public: int getA() const{ return a; } int getA(){ return a+1; } }; int main(){ A a1; const A a2; a1.getA();//调用的是普通的成员函数 a2.getA();//调用的是const修饰的函数 }
- 表示保证不会在此成员函数中修改成员变量的值(因为
使用地方
- 修饰函数的参数:
一般是修饰参数为引用类型的,当我们不想让这个实参被修改时,可以使得在函数内不会误改实参。 - 修饰返回值:
- 基本类型的返回值:为常量。
- 指针或引用类型的返回值:指向一个常量,其实也就是常引用,只能访问不能修改这个返回值
const int& a();
。
- 修饰修饰成员函数 表示不能在此成员函数中修改定义该函数类中的成员变量,具体原因就是
this
被const
修饰了,详情在目录this指针。
- 修饰函数的参数:
static
修饰符
可以修饰成员变量、局部变量、全局变量、成员函数、普通函数。
- 修饰局部变量 生命周期为整个程序结束后销毁,存放到全局区,内存中只有一份,编译期间就初始化好了。
- 修饰全局变量 生命周期也是整个程序销毁为止,存储在全局区共享一份,编译器初始化。但是这个全局变量变为只能在声明该变量的文件访问到。
- 修饰成员变量 与前两个类似,但是对于类来说的话是属于类的,通过类名访问
className::A
且只能在类外初始化。 - 修饰成员函数 也是属于类,通过类名访问,初始化没有要求,在
static
函数内不能访问非static
方法。 - 修饰普通函数 表明该函数只在定义该函数的类中可以使用,可以防止命名冲突。
override
和final
(当做 Java 中的来理解就行)
一般是为了增强虚函数的安全性。下面为加在类的成员函数后面的意义。
override
是让编译器去检查我们在子类中覆写父类的方法时是否覆写正确。
最佳实践是只在父类中虚函数上添加virtual
而在子类中意图重写父类方法的地方都加上override
而不必关心是否需要加virtual
(因为加了前者编译器就会帮我们检查),只有好处没有坏处。final
是为了防止父类中的方法被子类重写。
最佳实践是在父类中不想被子类重写的虚函数加上此关键字。
final
还可以加在类上表示这个类不可以被继承。
this
指针
是非静态成员函数中的隐含参数其指向调用函数的那个对象(其实就是自引用),编译器会在这个函数被调用时将这个调用者的地址赋值给这个 this
指针。 this
会隐式使用。被 const
修饰的成员函数中 this
被修饰为 const className* const this
,即this
是一个右值,不能对其进行取址操作。
extern
关键字
用于将变量或函数的作用域延伸到本文件或其他模块,使用了static
标识的不行。例如: 头文件
//myhead.h 只在头文件中声明,为了防止重复定义
#ifndef "MYHEAD_H"
#define "MYHEAD_H"
extern int a;
extern int test();//extern可以省略,因为函数的声明与定义是可区别的
#endif
源文件
//model1.cpp 在此处进行定义
#include "myhead.h"
int a = 100;
int test(){
std::cout<<"你好,我是吗喽。";
}
其他源文件
//model2.cpp 在此处需要使用则用extern进行引入
extern a;
extern test;
int main(){
test();
std::cout<<a;
}
extern "C"
就是让其表示的语句是通过类 C 编译与链接规约来进行编译与链接的,可以实现 C 与 C++ 的混合编程(目的为此)。
哪种变量会有默认值
全局变量、全局静态变量、局部静态变量。由编译器在编译阶段执行赋值操作。
类型转换CAST
- 静态类型转换: 用于类型相似的对象之间的转换,如 int 转换为 float,不会在运行时进行型检查。
- 动态类型转换: 一般是基类的指针或引用转换为派生类的指针或引用,会在运行时进行类型查
- 常量转换: 用于将
const
类型对象转换为非const
类型的对象,只能用于转换掉const
属性不能改变对象的类型,前提是被转换的对象原本是非常量。 - 重新解释转换: 将一个数据类型解释为另一个数据类型的值。
智能指针
原理: 智能指针是一个类,用来存储指向动态分配内存的对象的指针,负责自动释放动态分配内存的对象,防止内存泄露。动态分配的资源交给一个类对象去管理,当智能指针类对象声明周期结束时自动调用析构函数释放资源。个人理解就是通过使用这种类型的指针去代替使用一般类型的裸指针,利用其自动管理的机制来防止内存泄露。
std::unique_ptr<T>
类型: 独占资源所有权的指针即只允许一个智能指针托管指向该资源(对象)的指针,是互斥性的,不支持普通拷贝和赋值操作,只能移动所有权,不能用在STL标准容器中。std::shared_ptr<T>
类型: 共享资源所有权的指针,通过引用计数器管理。多个智能指针可以指向相同对象,该对象和其相关资源会在最后一个引用被销毁时被释放,由最后一个智能指针执行。std::weak_ptr<T>
类型: 共享资源的观察者,需要和std::shared_ptr
一起使用,不影响资源的生命周期,创建不会使得关联资源的引用计数器+1,析构不会使得引用计数器-1,是弱引用,可以与shared_ptr
相互赋值。通过配合shared_ptr<T>
使用可以解决循环引用问题造成的内存泄露。
循环引用的解决:
#include <iostream>
#include <memory>
class A{
public:
shared_ptr<B> b;//5
};
class B{
public:
//shared_ptr<A> a;//9
weak_ptr<A> a;//10
};
int main{
shared_ptr<A> pa(new A);//13
shared_ptr<B> pb(new B);
pa.b = pb;//15
pb.a = pa;
}
在上面的场景中类A中有成员变量会引用类B而类B中也会有成员变量引用A。
如果类B中成员变量是shared_ptr<A> a;
的情况的话,那么 main 函数中13处会使得 pa 中的引用计数器pa.use_Count
的值为1(资源的创建),在14行处pb.use_Count==1
,15行处pb.use_Count==2
,16行处pa.use_Count==2
;程序结束 pa、pb被销毁,pa.use_Count
与pb.use_Count
都变为1,但是只有当这个引用计数器为0时,最后一个引用了资源的智能指针shared_ptr
才会去帮我们释放其引用的资源,所以导致A、B并不能被正确释放。
如果类B中成员变量是weak_ptr<A> a;
的话,那么到16行后程序未结束前,pa.use_Count==1
,pb.use_Count==2
(因为weak_ptr<T>
引用相同的资源不会引起引用计数器加1),程序结束后都分别减1,此时pa.use_Count==0
、pb.use_Count==1
,会将A释放,而A释放后其中的成员变量shared_ptr<B> b;
调用其析构函数就没了,从而会导致pb.use_Count==0
,B被正确释放。循环引用解决。通过观察其构造函数、拷贝构造函数、析构函数以及重载的运算符我们可以很清晰的了解其引用计数的变化情况。详见文章智能指针的实现
struct
与class
的区别
struct
可以看做是数据结构的实现体,而class
是对象的实现体,最本质的区别是默认的访问控制。默认的继承访问权限:struct
是 public
、class
是private
的。默认的数据访问权限(成员变量访问权限):struct
是public
、class
是private
的。
为什么拥有虚函数的父类其析构函数必须为虚析构函数?
答:因为在一个父类指针指向子类对象的时候(展现多态性时),当其析构函数不为虚函数时,使用delete
释放指针所指对象的内存资源的时候不会触发动态绑定,也就是说只会调用父类当中的析构函数从而会导致子类中的资源无法被释放造成内存泄露。如果在父类当中将析构函数定义为虚析构函数的话就会触发动态绑定,父类的虚指针就可以找到其子类当中的析构函数并先调用子类的析构函数,在子类的析构函数中会嵌套调用父类的析构函数,这样资源才能被正确释放。虚析构函数的作用也就为了能用一个父类的指针来删除其指向的派生类对象。
如何定义只能在堆/栈上存在的对象?
- 只能在栈上: 将类的 new 与 delete 重载为 private 修饰的,这样就无法在堆上开辟内存。
- 只能在堆上: 将类的析构函数设置为 private 的,因为编译器为类对象分配栈空间时会先检查对象的析构函数是否可用。
引用
看做是变量的别名,与绑定的变量共享相同的内存地址。将参数设置为引用类型可以防止实参被修改,还能省去对象的拷贝所带来的开销。 本质:在C++内部实现是一个指针常量。例如:int* const ref = &a;
,也说明了引用为什么被初始化之后就不能对其赋值了。
指针
指针也是一种数据结构,在32操作系统中大小为4字节64位操作系统中为8字节(可利用sizeof()
函数求得)。可以通过指针间接访问内存,指针存放的是地址,通过 *
解引用操作符可以操作指针指向的内存。
空指针:指向内存空间编号为0的空间(内存空间从0开始编号,一般用十六进制数表示)。
野指针:指针变量指向未知/非法的内存空间。
空指针和野指针都不是我们申请的空间,不要访问。
友元函数与友元类(友元修饰函数则是友元函数,修饰类则是友元类)
- 友元关系是单向的,且不可被传递、继承。
- 友元函数是在类中通过
friend
声明,类外定义,其不是任何类的成员函数(参数中没有this),可以在友元函数中访问该类中的private
、protected
成员变量。当要访问非 static 变量时需要传入该类对象作参数,访问 static 变量时则不需要传入对象作为参数。 - 友元类也是通过
friend
在类中声明,不是任何类的成员变量,这个类的所有成员函数都是另一个类的友元函数。
#include<iostream>
using namespace std;
class A{
private:
int a;
public:
static int b;
friend void f1(class& c);
friend class B;
};
void f1(class& c){
cout<<c.a<<A::b<<endl;
}
class B{
public:
void f2(class& c){
//通过对象访问了他的成员变量,通过类名访问了其的static变量
cout<<c.a<<A::b<<endl;
}
};
int main(){
A a;
B b;
f1(a);
b.f2(a);
}
优点:提高了程序的运行效率,最大限度的保证了类中数据成员的安全。 缺点:破坏了类的封装性。
Lambda表达式
一般用于匿名函数,结合STL的算法使用
[捕获列表](参数)->返回值类型{语句};
捕获列表可以捕获非静态成员函数中的this
写法:[]{}
;、[](int a){};
、[](int a)->int{return a};
内存模型
有代码区、全局区、堆区、栈区,编译后未执行exe之前只有代码区和全局区。
- 代码区 存放函数体的二进制代码(CPU执行的指令),由操作系统管理,是共享只读的。
- 全局区 全局区里面包含常量区。存放全局变量、静态变量、全局常量(静态常量在栈区)、字符串常量,程序结束后由操作系统释放相应数据内存。
- 栈区 存放函数的参数、局部变量等,由编译器自动分配释放。
- 堆区 存放
new
出来(动态分配内存)的对象,由程序员分配释放,程序结束后操作系统会回收未被释放的空间。
栈里面的内存由编译器管理,存放方法的局部变量、参数等。堆里面是存放动态开辟内存的对象,即使用new
实例化的对象,由程序员手动申请与释放。
预处理指令#ifndef
(if not define)、#endif
(结束标志)、ifdef
(if define)、#else
(分支)
预处理指令,用于条件编译,可以根据是否定义了某个宏来决定是否编译某段代码,一般在头文件中使用,可以防止重定义错误。即在多个.cpp文件中重复引用了该头文件导致多次编译头文件中的内容。
//myhead.h
#ifndef _MYHEAD_
#define _MYHEAD_
class A{
};
#endif
STL容器
分为:
- 序列式容器(元素可序但未必有序,允许双向遍历)
- vector(动态数组)
- deque(双端队列)
- list(双向链表)
- 无序容器。
- 关联式容器(存储键值对,通过键来组织元素)
- set(不允许重复元素,只有键)
- multiset(多重集合,允许多个元素具有相同的键)
- map(映射,一个键映射一个值)
- multimap(多重映射,允许多个键映射到相同的值)
array
固定大小的顺序容器。
vector
可变大小的数组序列容器。
自动扩容原理:申请空间、拷贝元素、释放空间。windows下是1.5倍,linux下是2倍。太大会浪费空间,太小会频繁扩容,2倍不能利用前面释放的内存空间而1.5倍经过多次扩容后可以利用前面释放的内存空间。
deque
双端队列,是一个具有动态大小的序列容器,可以在两端扩展或收缩。
forward_list
单向链表,是序列容器。
list
双向链表,是序列容器。
set
红黑树实现,按照特定顺序存储唯一元素的容器,是一种特殊的map只有键没有值。
unordered_set
通过哈希表计算元素位置,类似unordered_map,只有键没有值。
map (类似java中的TreeMap)
- 关联容器,按照一定顺序存储由key和value组合成的元素。
- 由红黑树实现,使用于有序数据的场景
unordered_map (类似java中的HashMap)
- 与map类似,但由哈希表实现(无序),查找效率高适用于需要高效查找的场景。