访问权限
基本原则
- public:公有访问权限。
- 公有成员可以被类的对象、类的派生类和类外部的代码访问。
- private:私有访问权限。
- 私有成员只能被类的成员函数访问。而不能被类的对象、类的派生类和类外部的代码访问。
- protected:保护访问权限。
- 保护成员可以被类的成员函数、派生类的成员函数访问。但不能被类的对象和类外部的代码访问。
- 共有继承、私有继承和保护继承:
- 派生类:无论哪种继承,派生类都可以访问基类的public、protected成员,不可以访问基类的private成员。
- 派生类对象:只能访问共有继承下的基类中的public成员。
- 共有继承:派生类对象只可以访问基类的public成员,不可以访问基类的protected、private成员。
- 私有继承和保护继承:派生类对象不可以访问基类任何成员。
C++三大特性:继承、封装、多态
继承
- 它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
- 常⻅的继承有三种方式:
- 实现(普通)继承: 指使用基类的属性和方法而无需额外编码的能力
- 接口继承: 指仅使用属性和方法的名称、但是子类必须提供实现的能力(纯虚函数)
- 可视继承: 指子窗体(类)使用基窗体(类)的外观和实现代码的能力
- 通过可视继承,派生类将继承基类的成员,并且这些成员在派生类中具有相同的访问权限,就像它们是派生类自己的成员一样。这意味着派生类的对象可以直接访问基类的成员,而无需使用作用域运算符(::)来引用基类的成员。
- 需要注意的是,可视继承将基类的成员作为派生类的公有成员继承,因此派生类可以访问基类的所有公有成员。但私有成员和保护成员在派生类中是不可直接访问的,需要通过基类的公有成员函数或友元函数来进行访问。
封装
- 把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏,例如:将公共的数据或方法使用public修饰,而不希望被访问的数据或方法采用private修饰。
- 确保用户代码不会无意间破坏封装对象的状态。
- 被封装的类的具体实现细节可以随时改变,无需调整用户级别的代码。
C++多态
- C++多态(Polymorphism)是一种面向对象编程(OOP)的特性,允许不同的对象通过共同的接口进行交互,从而实现代码的灵活性和可扩展性。
- C++中的多态性有两种主要形式:运行时多态(Runtime Polymorphism)和编译时多态(Compile-time Polymorphism)。
编译时多态:编译时多态是通过函数重载(Function Overloading)和模板(Template)来实现的。
- 编译时多态允许在编译时根据函数参数的类型来选择合适的函数实现,从而实现静态的函数调用。
运行时多态:运行时多态是通过虚函数(Virtual Function)来实现的。
- 我们可以将基类的指针或引用绑定到派生类的对象上。
- 在基类中,可以使用virtual关键字来标识一个函数为虚函数,从而使得派生类可以通过函数的覆盖(Override)来提供自己的实现。运行时多态允许在运行时根据实际对象的类型来调用相应的函数实现,从而实现动态的函数调用。
虚函数
- 当基类希望派生类定义适合自己的版本,就将这些函数声明成虚函数(virtual)
- 虚函数是动态绑定的:在运行时根据对象的实际类型来决定调用哪个函数的机制,也称为运行时多态。动态绑定允许在派生类和基类之间进行多态调用,使得程序能够根据对象的实际类型来选择调用不同的成员函数。动态绑定绑定的是动态类型:所对应的函数或属性依赖于对象的动态类型,发生在运行期。
- 使用基类的引用或指针调用虚函数时发生。
- 既可以使用基类对象,又可以使用派生类对象。
- 虚函数的工作方式:依赖虚函数表工作的,表来保存虚函数地址,当我们用基类指针指向派生类时,虚表指针vptr指向派生类的虚函数表。
- 纯虚函数:实际上是将这个类定义为抽象类,不能实例化对象。
- 构造函数不能是虚函数。
- 析构函数可以是虚函数、纯虚函数:
- 在一个复杂的类中,析构函数通常是虚函数。
- 析构函数可以是纯虚函数,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。
- inline, static, constructor三种函数都不能带有virtual关键字。
- inline是在编译时展开,必须要有实体:内联函数是指在编译期间用被调用函数体本身来代替函数的调用指令,但虚函数的多态特性需要在运行时根据对象类型才知道调用哪个虚函数,所以没法在编译时进行内联函数展开。
- static属于class自己的类相关,必须有实体:static成员没有this指针。virtual函数一定要通过对象来调用,有隐藏的this指针,实例相关。
- 派生类的override虚函数定义必须和父类完全一致:除了一个特例,如果父类中返回值是一个指针或引用,子类override时可以返回这个指针(或引用)的派生。
虚继承
class B : public virtual A {
public:
// ...
};
- 定义:C++中的虚继承(virtual inheritance)是一种继承方式,用于解决多重继承带来的问题。在C++中,当一个类从多个基类继承时,如果其中的某个基类在多个路径上被继承,就会导致派生类中存在多个相同的子对象,从而可能导致二义性和资源重复释放等问题。虚继承通过引入虚基类(virtual base class)的概念来解决这些问题。虚继承使用关键字"virtual"来标识基类。
- 多重继承:是指一个类从多个基类继承而来,而虚继承则是在多重继承中的一种特殊方式。使用多继承经常出现二义性,必须十分小心; 一般只有在比较简单和不易出现二义性或者实在必要情况下才使用多继承,能用单一继承解决问题就不要用多继承。
- 为什么需要虚继承
- 为了解决多继承时的命名冲突和冗余数据问题:C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。
- 虚继承的目的是让某个类做出声明,承诺愿意共享它的基类:其中,这个被共享的基类就称为虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
- 虚继承实例:C++标准库中的 iostream 类就是一个虚继承的实际应用案例。
- iostream类 从 istream类 和 ostream类 直接继承而来,而 istream类 和 ostream类 又都继承自一个共同的名为 baseios 类,是典型的菱形继承。此时 istream类 和 ostream类 必须采用虚继承,否则将导致 iostream类 中保留两份 baseios 类的成员。
空类
class EmptyClass {
// 该类没有任何成员变量或成员函数
};
- C++中的空类(Empty Class)是指:没有任何成员变量或成员函数的类。
- 它仍然会占据内存空间:在C++中,每个对象都会占用至少一个字节的内存空间,这是由于对齐和内存对齐的规则所导致的。
- 空类在继承和多态方面可能会有一些特殊用途:例如,空类可以用作虚基类,用于在多继承中解决菱形继承(Diamond Inheritance)问题。此外,空类还可以用作标签类(Tagging Class)或标志类(Flag Class),用于在运行时对对象进行标记或分类。
抽象类与接口的实现
- 接口描述了类的行为和功能,而不需要完成类的特定实现;C++ 接口是使用抽象类来实现的 。
- 类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用 “= 0” 来指定的。
- 设计抽象类的目的:是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。
override 和 overload
- 重写与重载的本质区别是,加入了override的修饰符的方法,此方法始终只有一个被你使用的方法。
override:是重写(覆盖)了一个方法
以实现不同的功能,一般是用于子类在继承父类时,重写父类方法。
- 规则:
- 重写方法的参数列表,返回值,所抛出的异常与被重写方法一致。
- 被重写的方法不能为 private
- 静态方法不能被重写为非静态的方法
- 重写方法的访问修饰符一定要大于被重写方法的访问修饰符(public>protected>default>private)
overload:是重载,这些方法的名称相同而参数形式不同
一个方法有不同的版本,存在同于一个类中。
- 规则:
- 不能通过访问权限、返回类型、抛出的异常进行重载
- 不同的参数类型:可以是不同的参数类型(参数类型必须不一样) ,不同的参数个数,不同的参数顺序
- 方法的异常类型和数目不会对重载造成影响
C++类型检查
- C++类型检查:指的是在编译时或运行时对C++程序中的数据类型进行验证的过程。
智能指针:shared_ptr 、unique_ptr、weak_ptr
shared_ptr:
- 实现机制:是在拷⻉构造时使用同一份引用计数
- 模板指针T* ptr:指向实际的对象
- 引用计数:必须new出来的,不然会多个shared_ptr里面会有不同的引用次数而导致多次delete
- 重载operator*和operator->:使得能像指针一样使用shared_ptr
- 重载copy constructor(拷⻉构造函数) :使引用次数加一
- 重载operator=(赋值运算符):如果原来的shared_ptr已经有对象,则让其引用次数减一并判断引用是否为零(是否调用delete);然后将新的对象引用次数加一
- 重载析构函数:使引用次数减一并判断引用是否为零(是否调用delete)
unique_ptr
- unique_ptr唯一拥有其所指对象:同一时刻只能有一个unique_ptr指向给定对象,离开作用域时,若其指向对象,则将其所指对象销毁(默认delete)。
- 定义unique_ptr时:需要将其绑定到一个new返回的指针上。
- unique_ptr不支持普通的拷⻉和赋值(因为拥有指向的对象):但是可以拷⻉和赋值一个将要被销毁的unique_ptr;可以通过release或者reset将指针所有权从一个(非const)unique_ptr转移到另一个unique_ptr。
weak_ptr
- weak_ptr是为了配合shared_ptr而引入的一种智能指针:它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况,但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。
- 和shared_ptr指向相同内存:shared_ptr析构之后内存释放,在使用之前使用函数lock()检查weak_ptr是否为空指针。
C++强制类型转换
- 关键字:static_cast、dynamic_cast、reinterpret_cast和 const_cast
static_cast
- static_cast 是一种用于进行编译时类型转换的关键字。它允许在不同数据类型之间进行隐式或显式转换。它在编译时进行类型检查,能够在代码执行前捕获许多类型相关的错误。
- 没有运行时类型检查来保证转换的安全性
- 进行上行转换(把派生类的指针或引用转换成基类表示)是安全的
- 进行下行转换(把基类的指针或引用转换为派生类表示),由于没有动态类型检查,所以是不安全的。
- 使用:
- 用于基本数据类型之间的转换,如把int转换成char。
- 把任何类型的表达式转换成void类型。
dynamic_cast
- 用于在多态情况下进行类型转换。多态是指通过指向基类的指针或引用来操作派生类对象的能力。
- dynamic_cast 主要用于以下两种情况:
- 用于在运行时检查指针或引用是否可以安全地转换为目标类型。如果无法转换,dynamic_cast 将返回一个空指针(对于指针)或引发一个 std::bad_cast 异常(对于引用)。
- 用于进行向下转型,即将指向基类对象的指针或引用转换为指向派生类对象的指针或引用。这只有在基类指针或引用指向的实际对象是派生类对象时才是安全的。 在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。
- 转换后必须是类的指针、引用或者void,基类要有虚函数,可以交叉转换。*
- dynamic本身只能用于存在虚函数的父子关系的强制类型转换;对于指针,转换失败则返回nullptr,对于引用,转换失败会抛出异常。
reinterpret_cast
- 它可以将一个指针或引用转换为另一种指针或引用,而不考虑它们的类型之间是否兼容。
- 可以将整型转换为指针,也可以把指针转换为数组;可以在指针和引用里进行肆无忌惮的转换。
- reinterpret_cast的行为是非常底层和危险的,因为它可以绕过C++类型系统的类型检查,直接对内存进行位级别的操作。因此,使用reinterpret_cast应该非常谨慎,只在必要且了解其含义和风险的情况下使用。
const_cast
- 用于在某些情况下去除变量的 const 修饰符,从而允许对其进行修改,即将常量对象转换为非常量对象。
- 常量指针转换为非常量指针,并且仍然指向原来的对象。常量引用被转换为非常量引用,并且仍然指向原来的对象。去掉类型的const或volatile属性。
指针与引用
- 指针:存放某个对象的地址。
- 其本身就是变量(命了名的对象),本身就有地址,所以可以有指向指针的指针。
- 可变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变。
- 引用:即变量的别名
- 不可变,必须初始化。
- 不存在指向空值的引用,但是存在指向空值的指针。
define 和 typedef
define
- 只是简单的字符串替换,没有类型检查
- 是在编译的预处理阶段起作用
- 可以用来防止头文件重复引用
- 不分配内存,给出的是立即数,有多少次使用就进行多少次替换
typedef
- 定义类型别名。
typedef double wages;
- 有对应的数据类型,是要进行类型检查判断的:编译器会将其视为原类型的变量,进行相应的类型检查和类型转换。
- 是在编译、运行的时候起作用
- 在静态存储区中分配空间,在程序运行过程中内存中只有一个拷⻉
using
- 别名声明
using SI = sales_item;
define 和 inline
define
定义预编译时处理的宏,只是简单的字符串替换,无类型检查,不安全。
inline
- inline将内联函数编译完成生成了函数体直接插入被调用的地方。减少了压栈,跳转和返回的操作。没有普通函数调用时的额外开销。
- 内联函数是一种特殊的函数,会进行类型检查; 是对编译器的一种请求,编译器有可能拒绝这种请求;
C++中inline编译限制:
- 不能存在任何形式的循环语句
- 不能存在过多的条件判断语句
- 函数体不能过于庞大
- 内联函数声明必须在调用语句之前
new 和 malloc
描述 | new / delete | malloc / free |
---|---|---|
本质属性 | 运算符 | 标准库函数 |
内存分配大小 | 无须指定内存块大小 | 显式指定内存大小 |
类型安全 | 是 | 不是 |
关系 | new 封装了 malloc | |
分配失败时 | 抛出 bac_alloc 异常 | 返回NULL |
其他特点 | 分配释放内存+调用构造函数/析构函数 | 只分配释放内存 |
constexpr 和 const
- const 表示只读的语义,constexpr 表示常量的语义。
- constexpr 只能定义编译期常量,而 const 可以定义编译期常量,也可以定义运行期常量。
- 你将一个成员函数标记为constexpr,则顺带也将它标记为了const。如果你将一个变量标记为constexpr,则同样它是const的。但相反并不成立,一个const的变量或函数,并不是constexpr的。
constexpr
constexpr的好处:
- 为一些不能修改数据提供保障,写成变量则就有被意外修改的⻛险。
- 有些场景,编译器可以在编译时对constexpr的代码进行优化,提高效率。
- 相比宏来说,没有额外的开销,但更安全可靠。
constexpr变量
- 必须使用常量初始化
- 如果constexpr声明中定义了一个指针,constexpr仅对指针有效,和所指对象无关。
constexpr int *p = nullptr;
//常量指针 顶层const
constexpr函数
constexpr int new() {return 42;}
- constexpr函数是指能用于常量表达式的函数。
- 函数的返回类型和所有形参类型都是字面值类型,函数体有且只有一条return语句。
- 为了可以在编译过程展开,constexpr函数被隐式转换成了内联函数。
constexpr 构造函数
- 构造函数不能是const,但字面值常量类的构造函数可以是constexpr。
- constexpr构造函数必须有一个空的函数体,即所有成员变量的初始化都放到初始化列表中。对象调用的成员函数必须使用 constexpr 修饰。
const
- 左定值,右定向:指的是const在*的左还是右边
- 指针常量:
const int* d = new int(2)
; - 常量指针:
int *const e = new int(2);
- 指针常量:
- 顶层const:指针本身是常量;
- 底层const:指针所指的对象是常量;
- 若要修改const修饰的变量的值,需要加上关键字 volatile; 若想要修改const成员函数中某些与类状态无关的数据成员,可以使用 mutable 关键字来修饰这个数据成员;
static
用于定义静态成员或静态变量
- 静态成员变量:使用 static 关键字定义的类成员变量,属于类本身而不是类的实例对象。每个类的实例对象共享同一个静态成员变量的值。可以在类的声明中定义静态成员变量,但必须在类的定义外面进行初始化。
- 静态成员函数:使用 static 关键字定义的类成员函数,属于类本身而不是类的实例对象。静态成员函数可以通过类名直接调用,而不需要创建类的实例对象。静态成员函数只能访问类的静态成员变量和其他静态成员函数,不能访问类的非静态成员变量和非静态成员函数。
- 作用:实现多个对象之间的数据共享 + 隐藏,并且使用静态成员还不会破坏隐藏原则;默认初始化为0.
const 和 static
描述 | const | static |
---|---|---|
修饰常量(非类中) | 超出作用域空间会被释放;定义时必须初始化,无法更改;const形参可以接受const和非const类型的实参 | 函数执行后不会释放其存储空间 |
修饰成员变量 | 只在某个对象的生命周期内是常量;而对整个对象而言是可变;不能赋值,不能在类外定义;只能通过构造函数的参数初始化列表初始化,原因:因为不同的对象对其const数据成员的值可以不同,所以不能在类中声明时初始化 | 只能用在类定义体内部的声明,外部初始化,且不加static |
修饰成员函数 | 防止成员函数修改对象的内容(不能修改成员变量的值,但是可以访问),const对象不可以调用非const的函数:但是非const对象可以调用; | ①作为类作用域的全局函数(不能访问非静态数据成员和调用非静态成员函数)②没有this指针(不能直接存取非类的非静态成员,调用非静态成员函数)③不能声明为virtual |
decltype
- 选择并返回操作数的数据类型。
decltype (f()) sum = x;
extern
- 声明外部变量【在函数或者文件外部定义的全局变量】
volatile
- 在C++编程中,关键字 “volatile” 是用来标识一个变量可能会在未经通知的情况下发生变化,从而告诉编译器不要对该变量进行优化或缓存。“volatile” 关键字通常用于多线程或并发编程中,以确保在存在并发修改的情况下,始终访问到变量的最新值。“volatile” 关键字的主要作用是告诉编译器不要对标记为 “volatile” 的变量进行编译优化,以保证变量的值在编译器优化时不被缓存。这样可以避免由于编译器优化导致的对变量值的错误访问,尤其在多线程环境下。
- 作用:指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值,保证对特殊地址的稳定访问
- 使用场合:在中断服务程序和cpu相关寄存器的定义
- 举例说明:
- 空循环:
for( volatile int i = 0 ; i < 100 ; i++ );
// 它会执行,不会被优化掉
- 空循环:
std::atomic
a++ 和 int a = b 在C++中是否是线程安全的?
在 C++ 中,a++ 和 int a = b 都不是线程安全的操作,因为它们都涉及到对共享变量的读写操作,而在多线程环境中,多个线程可能同时访问这些共享变量,导致竞态条件(Race Condition)的发生,从而产生未定义行为。
-
a++ 操作实际上包含了读取 a 的值、增加其值、写回新的值这三个步骤,而这三个步骤不是原子操作,即它们不能在多线程环境中同时执行,可能会导致多个线程同时读取到相同的值,并对其进行加一操作,从而导致结果不符合预期。
-
int a = b 操作涉及到对变量 a 进行赋值操作,而在多线程环境中,如果有其他线程同时对 a 进行读写操作,可能导致 a 的值被不同的线程同时修改,从而导致 a 的值不确定。
对整形变量原子操作的相关库:atomic
-
"atomic"是一种用于多线程编程的特殊关键字,用于实现线程安全的原子操作。在多线程环境中,多个线程可以同时访问和修改共享的内存位置,这可能导致竞态条件和数据访问冲突。使用"atomic"关键字可以确保对共享变量的读写操作是原子的,即不会被其他线程中断,从而保证线程安全性。
-
std::atomic<int> value; value = 99;