目录
面向对象的三大特性
C++面向对象的三大特征分别是封装、继承和多态。
封装:对象的数据以及方法封装在一起,保护内部方法和数据,只暴露必要的接口,目的就是增加代码的安全性以及维护性。
class Person {
private:
string name;
int age;
public:
Person(string n, int a) : name(n), age(a) {}
void display() {
cout << "Name: " << name << ", Age: " << age << endl;
}
};
继承:派生类从基类中继承的属性和行为,从而实现代码的重用和拓展。子类通过继承父类,可以实现拓展或者修改父类的功能。
class Animal {
public:
void eat() {
cout << "Eating..." << endl;
}
};
class Dog : public Animal {
public:
void bark() {
cout << "Barking..." << endl;
}
};
多态:不同情况下,调用同一个函数的时候,会出现不同的行为。多态主要是通过函数重载、运算符重载以及虚函数实现的。注意,虚函数和继承结合使用,可以实现运行时的动态多态,就是通过基类指针或者引用调用派生类的函数
class Shape {
public:
virtual void draw() {
cout << "Drawing shape" << endl;
}
};
class Circle : public Shape {
public:
void draw() override {
cout << "Drawing circle" << endl;
}
};
class Square : public Shape {
public:
void draw() override {
cout << "Drawing square" << endl;
}
};
void render(Shape& shape) {
shape.draw(); // 根据传入对象的类型,调用不同的 draw 方法
}
重载、重写和隐藏
三者主要区别概述
- 重载:同名函数在同一作用域内可以有不同的参数列表
- 重写:派生类重新定义基类中的虚函数,实现动态多态
- 隐藏:派生类中的同名成员隐藏了基类中的成员,除非使用基类作用域显式调用
重载:统一作用域内,允许多个同名函数存在,但是这些函数的参数列表是不同的。重载函数可以在同一个类中,也可以是全局函数,编译器会根据函数的参数列表来区分以及调用不同的重载函数
class Print {
public:
void display(int i) {
cout << "Integer: " << i << endl;
}
void display(double d) {
cout << "Double: " << d << endl;
}
void display(string s) {
cout << "String: " << s << endl;
}
};
重写:派生类重新定义基类中已经存在的虚函数。子类重写的函数必须和父类中虚函数一模一样。通过重写以及派生类可以提供特定的实现,并且在运行时可以通过基类指针或者引用调用派生类的重写函数(动态多态的实现)
class Base {
public:
virtual void show() {
cout << "Base show" << endl;
}
};
class Derived : public Base {
public:
void show() override { // 重写基类的 show 函数
cout << "Derived show" << endl;
}
};
隐藏:派生类中定义与基类同名的非虚函数或者变量,此时派生类就会隐藏基类中的同名成员。隐藏的成员函数或者变量在派生类中不会被直接访问到,除非使用基类作用域来显式的调用。
class Base {
public:
void display() {
cout << "Base display" << endl;
}
};
class Derived : public Base {
public:
void display() { // 隐藏基类的 display 函数
cout << "Derived display" << endl;
}
};
多态及其实现原理
多态就是指同一个函数在不同情况下表现出不同的行为。C++中的多态主要有两类,一类是静态多态(编译时多态),也就是通过函数重载和运算符重载实现。一类则是动态多态(运行时多态),通过虚函数和继承实现。动态多态的实现则是主要依赖于虚函数表(存储虚函数指针)以及虚函数指针(指向虚函数表)实现。
静态多态(编译时多态):典型的实现就是函数重载以及运算符重载了,也及时在编译的时候就确定使用哪个函数
class Print {
public:
void display(int i) {
cout << "Integer: " << i << endl;
}
void display(double d) {
cout << "Double: " << d << endl;
}
void display(string s) {
cout << "String: " << s << endl;
}
};
动态多态(运行时多态):典型是通过虚函数和继承实现。
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() {
cout << "Base show" << endl;
}
virtual ~Base() {}
};
class Derived : public Base {
public:
void show() override {
cout << "Derived show" << endl;
}
};
int main() {
Base* b = new Derived();
b->show(); // 调用 Derived 类的 show 函数
delete b;
return 0;
}
动态多态实现原理分析:当程序运行时,通过基类指针或者引用调用虚函数时,程序会通过对象的虚函数指针找到对应的虚函数表,然后从虚函数表中获取实际需要调用的函数地址,并执行该函数。
- 虚函数表:存储虚函数地址
- 虚函数指针:指向虚函数表
虚函数和纯虚函数
两者区别
- 虚函数:基类中声明并提供默认实现,派生类可以重写也可以不重写。运行时通过基类指针或者引用调用,可以调用派生类的版本
- 纯虚函数:基类中声明但是不实现,派生类必须对其进行重写。纯虚函数会让这个类称为抽象类,不能够实例化对象,只作为接口使用。
虚函数
- 用关键字virtual声明的成员函数
- 作用:允许在派生类中重写,从而实现动态多态,直接使用基类指针或者引用就可以特定的对象,而不是指针或者引用的版本
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() {
cout << "Base show" << endl;
}
virtual ~Base() {} // 虚析构函数,确保正确析构派生类对象
};
class Derived : public Base {
public:
void show() override {
cout << "Derived show" << endl;
}
};
int main() {
Base* b = new Derived();
b->show(); // 调用 Derived 类的 show 函数
delete b;
return 0;
}
纯虚函数
- 纯虚函数就是在虚函数后面加一个 = 0,用于在基类中声明但是不实现
- 包含纯虚函数的类就是抽象类,这个类是不可以被实例化
- 纯虚函数强制派生类必须提供具体的实现,也就是子类必须实现基类的这个接口
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() = 0; // 纯虚函数
virtual ~Base() {}
};
class Derived : public Base {
public:
void show() override {
cout << "Derived show" << endl;
}
};
int main() {
// Base b; // 错误:无法实例化抽象类
Base* b = new Derived();
b->show(); // 调用 Derived 类的 show 函数
delete b;
return 0;
}
虚函数的实现机制
具体实现简述
- 定义类和虚函数表创建
- 如果一个类中含有虚函数,那么编译器就会给这个类生成一个虚函数表
- 虚函数表中存储着这个类所有的虚函数地址,如果一个类继承了基类,并重写了基类虚函数。虚函数表中相应的位置会存储派生类的虚函数地址
- 对象创建以及虚函数指针初始化分析
- 当创建包包含虚函数的对象时,对象中会包含一个隐藏的虚函数指针
- 对象构造初始化的时候,构造函数会初始化这个虚函数指针,使其指向该类的虚函数表
- 虚函数调用
- 通过基类指针或者引用时,程序会先通过对象虚函数指针找到虚函数表
- 然后,根据虚函数表中的地址,调用具体函数的实现
构造函数和析构函数是否可以改为虚函数
- 构造函数不能是虚函数:对象创建阶段是无法进行多态调用的
- 析构函数可以是虚函数:具有继承关系的类中,基类的析构函数应当声明为虚函数,目的就是为了确保通过基类指针删除派生类对象时,能够正确调用派生类的析构函数,从而释放所有资源。
构造函数不能是虚函数分析
- 构造函数初始化对象的时候,构造函数被调用,但是此时对象的内存还没有初始化完成,这也就意味着虚函数表和虚函数指针没有设置完毕,所以此时肯定无法进行多态
- 构造函数时,必须需要知道确切的类型,但是虚函数则是运行期间动态的根据对象类型进行绑定,所以这与构造函数是冲突的
析构函数可以是虚函数分析
- 保证基类和派生类对象都正确释放:如果基类的析构函数不设置为虚函数,那么当基类指针删除派生类对象时,只会调用基类的析构函数,而派生类的析构函数不会被调用。这样就会导致派生类资源没有正确释放,从而导致内存泄漏等
- 删除一个对象的时候,必须确保所有派生类析构函数都可以被正确调用,所以声明基类的析构函数为虚函数,就是为了保证在删除基类指针时,调用实际对象的析构函数,从而保证所有资源的正确释放。
#include <iostream>
using namespace std;
class Base {
public:
Base() {
cout << "Base constructor" << endl;
}
virtual ~Base() { // 虚析构函数
cout << "Base destructor" << endl;
}
};
class Derived : public Base {
public:
Derived() {
cout << "Derived constructor" << endl;
}
~Derived() {
cout << "Derived destructor" << endl;
}
};
int main() {
Base* b = new Derived();
delete b; // 调用 Derived 的析构函数,然后调用 Base 的析构函数
return 0;
}
深拷贝和浅拷贝
深浅拷贝的区别
- 浅拷贝
- 源对象和目标对象共享同一块内存
- 修改一个对象数据会影响另一个对象数据
- 深拷贝
- 源对象和目标对象拥有独立的内存
- 修改一个对象数据不会影响另一个对象数据
深拷贝实现
- 拷贝构造:深拷贝的拷贝构造函数需要分配新的内存并且复制数据
- 赋值运算符重载:确保赋值操作同样进行深拷贝
- 资源管理:一定要确保分配的内存在析构函数中都获得正确释放,避免内存泄漏
class Deep {
public:
int* data;
Deep(int value) {
data = new int(value);
}
// 拷贝构造函数
Deep(const Deep& other) {
data = new int(*other.data);
}
// 赋值运算符重载
Deep& operator=(const Deep& other) {
if (this == &other) {
return *this; // 防止自我赋值
}
delete data; // 释放已有的内存
data = new int(*other.data); // 分配新内存并复制数据
return *this;
}
~Deep() {
delete data;
}
void display() const {
cout << "Value: " << *data << endl;
}
};
浅拷贝:直接复制其数值,类似于抄别人作业将名字也抄上去,一个名字可以找到两个人
#include <iostream>
using namespace std;
class Shallow {
public:
int* data;
Shallow(int value) {
data = new int(value);
}
// 拷贝构造函数实现浅拷贝
Shallow(const Shallow& other) : data(other.data) {}
~Shallow() {
delete data;
}
void display() const {
cout << "Value: " << *data << endl;
}
};
int main() {
Shallow obj1(42);
Shallow obj2 = obj1; // 浅拷贝
obj2.display();
*obj1.data = 100;
obj2.display(); // obj2 的值也变了,因为它们共享同一个内存地址
return 0;
}
单继承和多继承虚函数表结构
两者结构区别
- 单继承:每个类只有一个虚函数表,派生类继承基类的虚函数表,并且会根据自己是否重写,而对该函数表进行修改
- 多继承:每个基类都有一个虚函数表,派生类合并且修改这些虚函数表。派生类对象中有多个虚函数指针,分别指向各个基类的虚函数表
单继承中虚函数表结构
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() {
cout << "Base show" << endl;
}
virtual ~Base() {}
};
class Derived : public Base {
public:
void show() override {
cout << "Derived show" << endl;
}
virtual void extra() {
cout << "Derived extra" << endl;
}
};
int main() {
Base* b = new Derived();
b->show(); // 调用 Derived 的 show 函数
delete b;
return 0;
}
多继承结构
#include <iostream>
using namespace std;
class Base1 {
public:
virtual void show1() {
cout << "Base1 show1" << endl;
}
virtual ~Base1() {}
};
class Base2 {
public:
virtual void show2() {
cout << "Base2 show2" << endl;
}
virtual ~Base2() {}
};
class Derived : public Base1, public Base2 {
public:
void show1() override {
cout << "Derived show1" << endl;
}
void show2() override {
cout << "Derived show2" << endl;
}
};
int main() {
Derived d;
Base1* b1 = &d;
Base2* b2 = &d;
b1->show1(); // 调用 Derived 的 show1 函数
b2->show2(); // 调用 Derived 的 show2 函数
return 0;
}
禁止构造函数
两种方法以及应用场景
- 将构造函数声明在Private作用域中
- 控制对象创建,例如单例模式下只创建一个类对象
- 通过类静态成员函数创建和获取对象实例(因为静态成员函数也只初始化一次)
- 构造函数删除
- 明确禁用构造函数即可
构造函数声明为Private
#include <iostream>
using namespace std;
class MyClass {
private:
MyClass() {
cout << "Constructor called" << endl;
}
public:
static MyClass& getInstance() {
static MyClass instance;
return instance;
}
void display() {
cout << "Display method" << endl;
}
// 禁止拷贝构造和赋值操作
MyClass(const MyClass&) = delete;
MyClass& operator=(const MyClass&) = delete;
};
int main() {
// MyClass obj; // 错误:构造函数是私有的,不能在类外部创建对象
MyClass& obj = MyClass::getInstance();
obj.display();
return 0;
}
delete关键字禁用默认构造函数(C++11以后)
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass() = delete; // 禁止默认构造函数
MyClass(int value) {
cout << "Parameterized constructor called with value: " << value << endl;
}
void display() {
cout << "Display method" << endl;
}
// 禁止拷贝构造和赋值操作
MyClass(const MyClass&) = delete;
MyClass& operator=(const MyClass&) = delete;
};
int main() {
// MyClass obj; // 错误:默认构造函数被删除
MyClass obj(10); // 可以调用带参数的构造函数
obj.display();
return 0;
}
减少构造函数开销方法
初始化列表:直接在成员变量创建的时候就初始化,从而避免不必要的默认构造和赋值操作,从而提高效率。
class MyClass {
public:
MyClass(int a, int b) : x(a), y(b) {} // 使用初始化列表
private:
int x;
int y;
};
减少临时对象的创建,尽量使用引用或者指针,减少拷贝对象的使用
void process(const MyClass& obj) {
// 使用引用,避免拷贝
}
// 调用时使用引用
MyClass obj;
process(obj);
内存池:事先分配好一定数量内存,从而减少频繁分配和释放内存开销
#include <vector>
class MemoryPool {
public:
MemoryPool(size_t size) {
pool.resize(size);
for (auto& block : pool) {
freeBlocks.push_back(&block);
}
}
void* allocate() {
if (freeBlocks.empty()) return nullptr;
void* block = freeBlocks.back();
freeBlocks.pop_back();
return block;
}
void deallocate(void* block) {
freeBlocks.push_back(static_cast<Block*>(block));
}
private:
struct Block {};
std::vector<Block> pool;
std::vector<Block*> freeBlocks;
};
// 自定义 new 和 delete 运算符
class MyClass {
public:
static void* operator new(size_t size) {
return pool.allocate();
}
static void operator delete(void* ptr) {
pool.deallocate(ptr);
}
private:
static MemoryPool pool;
};
MemoryPool MyClass::pool(100); // 初始化内存池
智能指针:有效管理对象生命周期,尽力避免内存泄漏的出现
#include <memory>
class MyClass {
// 类定义
};
void createObject() {
std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>();
// 使用智能指针管理对象
}
移动语义:直接移动数值,避免拷贝
class MyClass {
public:
MyClass(MyClass&& other) noexcept : data(other.data) {
other.data = nullptr; // 移动构造函数
}
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
delete data;
data = other.data;
other.data = nullptr;
}
return *this; // 移动赋值运算符
}
private:
int* data;
};
类对象初始化的顺序
初始化顺序总结
- 基类部分初始化
- 成员对象初初始化,依据类中声明顺序进行初始化,注意不是在初始化列表中出现的顺序
- 构造函数体初始化
#include <iostream>
using namespace std;
class A {
public:
A() {
cout << "A's constructor called" << endl;
}
};
class B {
public:
B() {
cout << "B's constructor called" << endl;
}
};
class C : public A {
public:
B b;
int x;
C() : x(10) {
cout << "C's constructor called" << endl;
}
};
int main() {
C c;
return 0;
}
构造函数的调用顺序
构造函数调用顺序总结
- 基类构造函数,从顶层基类函数开始依次向下调用
- 成员对象构造函数
- 当前类的构造函数
#include <iostream>
using namespace std;
class Base1 {
public:
Base1() {
cout << "Base1's constructor called" << endl;
}
};
class Base2 {
public:
Base2() {
cout << "Base2's constructor called" << endl;
}
};
class Derived : public Base1, public Base2 {
public:
Derived() {
cout << "Derived's constructor called" << endl;
}
};
int main() {
Derived d;
return 0;
}
类对象的初始化顺序
初始化顺序遵循成员在类中声明的顺序
#include <iostream>
using namespace std;
class A {
public:
A(int a) {
cout << "A's constructor called with " << a << endl;
}
};
class B {
public:
B(int b) {
cout << "B's constructor called with " << b << endl;
}
};
class C {
public:
A a;
B b;
C(int x, int y) : b(y), a(x) { // 初始化列表顺序与声明顺序不同
cout << "C's constructor called" << endl;
}
};
int main() {
C c(1, 2);
return 0;
}
类的成员初始化
初始化方法总结
- 构造函数初始化列表:适用于所有成员变量,特别是常量和引用成员
- 类内初始化:确保成员变量在构造函数调用之前得到初始化
- 默认成员初始化:为成员变量提供默认值
- 注意,常量和引用成员的初始化必须在初始化列表中进行初始化
常量与引用成员必须在初始化列表中初始化
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass(int a, int& b) : x(a), ref(b) { // 初始化列表初始化常量和引用
cout << "Constructor called" << endl;
}
void display() const {
cout << "x: " << x << ", ref: " << ref << endl;
}
private:
const int x;
int& ref;
};
int main() {
int value = 20;
MyClass obj(10, value);
obj.display();
value = 30;
obj.display(); // ref 引用的值也会更新
return 0;
}
默认成员初始化
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass() : x(10), y(20) { // 使用默认值初始化成员
cout << "Default constructor called" << endl;
}
MyClass(int a) : x(a), y(20) { // 初始化 y 为默认值
cout << "Parameterized constructor called" << endl;
}
void display() const {
cout << "x: " << x << ", y: " << y << endl;
}
private:
int x;
int y;
};
int main() {
MyClass obj1;
obj1.display();
MyClass obj2(30);
obj2.display();
return 0;
}
类内初始化(只适合C++11以后使用)
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass() {
cout << "Constructor called" << endl;
}
void display() const {
cout << "x: " << x << ", y: " << y << endl;
}
private:
int x = 10; // 类内初始值
int y = 20; // 类内初始值
};
int main() {
MyClass obj;
obj.display();
return 0;
}
成员初始化列表为什么效率高
原因总结
- 避免不必要的默认构造和赋值操作,从而提高初始化效率
- 不用先调用默认构造函数然后再进行赋值操作,这样就避免了不必要临时对象创建和效率,从而减少性能开销
- 常量以及引用必须在初始化列表中初始化
- 直接调用成员对象的构造函数,避免额外的构造和赋值操作
- 编译器可以更好的优化初始化代码,从而提高性能
- 因为利用初始化列表,编译器可以直接生成初始化代码,而不需要先生成构造函数调用然后再生成赋值代码
- 通过该优化,可以减少对象的构造和赋值次数,从而提高代码效率
- 确保成员变量在构造函数体执行前被正确的初始化
- 因为某些变量需要在构造函数执行之前就要初始化,而初始化列表可以保证在构造之前初始化完成
友元函数作用及其使用场景
友元函数可以访问类的私有成员以及保护成员。但是一定要注意谁是谁的朋友。
友元函数作用
- 访问私有和保护成员:友元函数可以访问类的私有和保护成员
- 提供全局函数的类成员访问权限:可以使非成员函数具备成员函数的访问权限
- 提高操作符重载的灵活性,通过友元函数可以访问私有和保护成员
【场景一】全局函数作为友元函数:MyClass类声明printData是自己的朋友,那么printDate就可以访问MyClass类中的成员,但是printData不一定是MyClass的朋友
#include <iostream>
using namespace std;
class MyClass {
private:
int data;
public:
MyClass(int value) : data(value) {}
// 声明友元函数
friend void printData(const MyClass& obj);
};
// 定义友元函数
void printData(const MyClass& obj) {
cout << "Data: " << obj.data << endl; // 访问私有成员 data
}
int main() {
MyClass obj(10);
printData(obj); // 调用友元函数
return 0;
}
【场景二】两个类需要相互访问私有成员,当两个类需要互相访问对方的私有成员时,就可以将对方的函数声明为对方的友元函数
#include <iostream>
using namespace std;
class ClassB; // 前向声明
class ClassA {
private:
int valueA;
public:
ClassA(int value) : valueA(value) {}
// 声明 ClassB 的成员函数为友元函数
friend void showValues(const ClassA& a, const ClassB& b);
};
class ClassB {
private:
int valueB;
public:
ClassB(int value) : valueB(value) {}
// 定义友元函数
friend void showValues(const ClassA& a, const ClassB& b) {
cout << "ValueA: " << a.valueA << ", ValueB: " << b.valueB << endl;
}
};
int main() {
ClassA a(5);
ClassB b(10);
showValues(a, b); // 调用友元函数
return 0;
}
【场景三】重载运算符,重载运算符需要访问私有成员时,就可以将这些成员声明为友元函数
#include <iostream>
using namespace std;
class Complex {
private:
double real;
double imag;
public:
Complex(double r, double i) : real(r), imag(i) {}
// 声明友元函数以重载 << 操作符
friend ostream& operator<<(ostream& out, const Complex& c);
};
// 定义友元函数以重载 << 操作符
ostream& operator<<(ostream& out, const Complex& c) {
out << "(" << c.real << ", " << c.imag << ")";
return out;
}
int main() {
Complex c(3.0, 4.5);
cout << c << endl; // 使用重载的 << 操作符
return 0;
}
避免拷贝的方法
1. 引用和指针,通过引用和指针实现仅存储对象地址
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass(int val) : value(val) {}
void display() const {
cout << "Value: " << value << endl;
}
private:
int value;
};
void processObject(const MyClass& obj) { // 使用引用
obj.display();
}
int main() {
MyClass obj(10);
processObject(obj); // 传递引用,避免拷贝
return 0;
}
2. 移动语义
#include <iostream>
#include <utility>
using namespace std;
class MyClass {
public:
MyClass(int val) : value(new int(val)) {
cout << "Constructor called" << endl;
}
// 移动构造函数
MyClass(MyClass&& other) noexcept : value(other.value) {
other.value = nullptr;
cout << "Move constructor called" << endl;
}
~MyClass() {
delete value;
}
void display() const {
if (value) {
cout << "Value: " << *value << endl;
} else {
cout << "Value is nullptr" << endl;
}
}
private:
int* value;
};
void processObject(MyClass obj) { // 接收右值
obj.display();
}
int main() {
MyClass obj(10);
processObject(std::move(obj)); // 使用std::move避免拷贝
obj.display(); // obj的资源已被移动
return 0;
}
3. 智能指针
#include <iostream>
#include <memory>
using namespace std;
class MyClass {
public:
MyClass(int val) : value(val) {}
void display() const {
cout << "Value: " << value << endl;
}
private:
int value;
};
void processObject(unique_ptr<MyClass> obj) { // 使用智能指针
obj->display();
}
int main() {
unique_ptr<MyClass> obj = make_unique<MyClass>(10);
processObject(std::move(obj)); // 使用std::move避免拷贝
return 0;
}
4. 禁用拷贝构造和赋值运算符
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass(int val) : value(val) {}
// 禁用拷贝构造函数和拷贝赋值运算符
MyClass(const MyClass&) = delete;
MyClass& operator=(const MyClass&) = delete;
void display() const {
cout << "Value: " << value << endl;
}
private:
int value;
};
void processObject(MyClass&& obj) { // 使用右值引用
obj.display();
}
int main() {
MyClass obj(10);
processObject(std::move(obj)); // 使用std::move避免拷贝
return 0;
}
拷贝构造函数必须声明为引用的原因
原因总结
- 避免无限递归调用:拷贝构造函数如果不是引用,而是值传递,那么在调用这个拷贝构造函数的时候,会试图将参数对象传递给函数。但是这个传递过程本身就会调用拷贝构造函数,这样就导致了无限递归,最终引起堆栈溢出
- 提高效率:通过值传递,可以减少在传递过程中复制出来的副本,从而提高效率
#include <iostream>
class MyClass
{
//MyClass()
//:value(10)
//{}
public:
int value;
// 拷贝构造函数,参数为引用
MyClass(const MyClass& obj) {
value = obj.value;
std::cout << "Copy constructor called" << std::endl;
}
public:
MyClass(){}
};
int main() {
MyClass obj1;
obj1.value = 10;
// 使用拷贝构造函数创建新对象 obj2
MyClass obj2 = obj1; // 这里会调用拷贝构造函数
std::cout << "obj2.value = " << obj2.value << std::endl;
return 0;
}
无限递归事例:如果直接传值,会导致无限递归,因为传值过程本身需要调用拷贝构造函数来传递参数
#include <iostream>
class MyClass {
public:
int value;
// 错误的拷贝构造函数:参数按值传递,而不是引用
MyClass(MyClass obj) { // 注意:这里的参数类型应该是const MyClass&,而不是MyClass
value = obj.value;
std::cout << "Copy constructor called" << std::endl;
}
};
int main() {
MyClass obj1; // 正常构造
obj1.value = 10;
// 试图拷贝构造对象
// 这将导致无限递归,因为每次调用MyClass的拷贝构造函数时,会再次调用它自己,直到栈溢出
MyClass obj2 = obj1;
return 0;
}
什么时候会调用拷贝构造函数
场景总结
- 对象以值传递方式传递给函数:当一个对象作为参数,以值传递方式传递给一个函数,会调用拷贝构造函数来创建这个对象的复制信息
- 对象以值传递的方式从函数返回:当一个函数以值传递的方式返回一个对象时,构造函数会被调用来创建返回值的副本
- 初始化对象时用同类的另一个对象进行初始化:Class Name obj2 = obj1
- 对象作为函数的返回值时
值传递对象给函数(这样会导致无限递归)
#include <iostream>
void function(MyClass obj) { // 这里会调用拷贝构造函数
std::cout << "Function called with value: " << obj.value << std::endl;
}
int main() {
MyClass obj1;
obj1.value = 20;
function(obj1); // 传递 obj1 时调用拷贝构造函数
return 0;
}
对象以值传递方式从函数返回(重点说明,非完整代码)
MyClass createObject() {
MyClass obj;
obj.value = 30;
return obj; // 返回时调用拷贝构造函数
}
int main() {
MyClass obj2 = createObject(); // 这里会调用拷贝构造函数
std::cout << "obj2.value = " << obj2.value << std::endl;
return 0;
}
初始化对象时使用类的另一个对象进行初始化时
int main() {
MyClass obj1;
obj1.value = 40;
MyClass obj2 = obj1; // 这里会调用拷贝构造函数
std::cout << "obj2.value = " << obj2.value << std::endl;
return 0;
}
如何禁止一个类被实例化
方法总结
- 构造函数声明为private 和 protected
- 将类声明为一个抽象类
- 删除构造函数,delete
- 所有成员声明为静态的,并将构造函数私有或者delete
造函数声明为private 和 protected(注意继承类是可以调用构造函数的)
class NonInstantiable {
private:
NonInstantiable() {} // 私有构造函数
};
int main() {
// NonInstantiable obj; // 这行代码会导致编译错误,因为构造函数是私有的
return 0;
}
class NonInstantiable {
protected:
NonInstantiable() {} // 受保护的构造函数
};
class Derived : public NonInstantiable {
public:
Derived() {} // 可以通过派生类构造函数调用
};
int main() {
// NonInstantiable obj; // 这行代码会导致编译错误,因为构造函数是受保护的
Derived derivedObj; // 这是允许的,因为Derived类的构造函数可以调用NonInstantiable的构造函数
return 0;
}
抽象类,避免初始化
class NonInstantiable {
public:
virtual void someFunction() = 0; // 纯虚函数
};
int main() {
// NonInstantiable obj; // 这行代码会导致编译错误,因为类是抽象的
return 0;
}
C++11以上,直接构造delete
class NonInstantiable {
public:
NonInstantiable() = delete; // 删除默认构造函数
};
int main() {
// NonInstantiable obj; // 这行代码会导致编译错误,因为构造函数被删除了
return 0;
}
静态成员类方法
class Utility {
private:
Utility() = delete; // 删除构造函数,禁止实例化
public:
static void someUtilityFunction() {
// 实用函数代码
}
};
int main() {
// Utility util; // 这行代码会导致编译错误,因为构造函数被删除了
Utility::someUtilityFunction(); // 可以直接通过类名调用静态成员函数
return 0;
}
实例化一个对象经历哪些阶段
主要过程分析
- 分配内存
- 局部对象在栈上分配空间
- new关键字则是在堆上分配
- 调用构造函数
- 初始化对象的成员变量
- 初始化成员变量
- 构造函数执行后,类的成员变量会按照初始化列表的顺序或在构造函数体内进行初始化
- 执行构造函数体
- 构造函数体内代码执行
- 调用基类构造函数(类存在继承的时候)
- 基类构造函数的调用顺序是从基类到派生类依次进行
- 返回对象内存地址
- 构造函数执行完成后,返回新创建对象的内存地址
- 析构函数释放空间
不允许修改类的成员变量的函数实现方法
成员函数声明为const成员函数实现即可,因为const成员函数表示该函数不能修改对象状态,也就是不可以修改任何成员变量
#include <iostream>
class MyClass {
private:
int value;
public:
MyClass(int v) : value(v) {}
// 常成员函数,不能修改类的成员变量
int getValue() const {
// this->value = 10; // 这行代码会导致编译错误,因为getValue是const成员函数
return value;
}
// 非常成员函数,可以修改类的成员变量
void setValue(int v) {
value = v;
}
};
int main() {
MyClass obj(5);
std::cout << "Value: " << obj.getValue() << std::endl; // 调用const成员函数
obj.setValue(10); // 调用非常成员函数
std::cout << "Value: " << obj.getValue() << std::endl;
return 0;
}
const成员函数使用场景
- const成员函数通常用于访问类的成员变量,但是不修改它们的状态
- 声明const成员函数可以使得这些函数在const对象上被调用
#include <iostream>
class MyClass {
private:
int value;
public:
MyClass(int v) : value(v) {}
// 常成员函数,不能修改类的成员变量
int getValue() const {
// this->value = 10; // 这行代码会导致编译错误,因为getValue是const成员函数
return value;
}
// 非常成员函数,可以修改类的成员变量
void setValue(int v) {
value = v;
}
};
int main() {
//MyClass obj(5);
//std::cout << "Value: " << obj.getValue() << std::endl; // 调用const成员函数
//obj.setValue(10); // 调用非常成员函数
//std::cout << "Value: " << obj.getValue() << std::endl;
const MyClass obj(5);
std::cout << "Value: " << obj.getValue() << std::endl; // 只能调用const成员函数
// obj.setValue(10); // 这行代码会导致编译错误,因为setValue不是const成员函数
return 0;
}
对象创建限制在堆或者栈
限制对象只可以在堆上创建:将类的析构函数声明为private 和 protected,因为栈上的对象会自动调用析构函数,而编译器会发现析构函数不可访问时产生错误
#include <iostream>
class HeapOnly {
private:
// 将析构函数声明为私有或受保护,防止栈上创建
~HeapOnly() {
std::cout << "HeapOnly object destroyed" << std::endl;
}
public:
// 提供静态成员函数用于删除对象
static void destroy(HeapOnly* p) {
delete p;
}
void show() {
std::cout << "HeapOnly object created" << std::endl;
}
};
int main() {
// HeapOnly obj; // 这行代码会导致编译错误,因为析构函数是私有的
HeapOnly* p = new HeapOnly(); // 只能在堆上创建
p->show();
HeapOnly::destroy(p); // 正确地释放堆上的对象
return 0;
}
限制对象只可以在栈上创建:将类的new 和 delete操作符重载为私有或者直接delete,从而禁止在堆上创建对象
#include <iostream>
class StackOnly {
public:
StackOnly() {
std::cout << "StackOnly object created" << std::endl;
}
~StackOnly() {
std::cout << "StackOnly object destroyed" << std::endl;
}
private:
// 将new和delete操作符重载为私有,防止在堆上创建对象
void* operator new(size_t) = delete;
void operator delete(void*) = delete;
};
int main() {
StackOnly obj; // 可以在栈上创建
// StackOnly* p = new StackOnly(); // 这行代码会导致编译错误,因为new操作符被删除了
return 0;
}
空类的字节数以及对应生成的成员函数
空类大小为1字节:目的是为了确保每个对象都有一个独特地址,尽管其没有任何实际数据
#include <iostream>
class Empty {};
int main() {
std::cout << "Size of Empty class: " << sizeof(Empty) << " bytes" << std::endl;
return 0;
}
空类也有默认成员函数
#include <iostream>
class Empty {};
int main() {
Empty e1; // 默认构造函数
Empty e2 = e1; // 拷贝构造函数
e2 = e1; // 拷贝赋值操作符
Empty e3 = std::move(e1); // 移动构造函数
e3 = std::move(e2); // 移动赋值操作符
return 0; // 析构函数在这里被调用
}
类的大小
影响因素
- 成员变量大小
- 取决于每个成员大小
- 填充字节
- 内存对齐
- 继承
- 如果类继承自另一个类,那么基类的大小也会包含在派生类大小中
- 如果含有虚函数,则会引入虚函数表指针,同样会增加类的大小
- 虚函数
- 如果类中有虚函数,则编译器会在对象中插入一个虚函数表指针(64位8字节)
- 静态成员变量
- 静态成员变量不会影响类实例的大小,因为静态成员变量部署需某个对象实例,而是属于整个类
- 空类大小
- 1
#include <iostream>
class Empty {};
class Simple {
public:
int a;
double b;
char c;
};
class Inherited : public Simple {
public:
char d;
};
class WithVirtual {
public:
virtual void func() {}
int a;
};
int main() {
std::cout << "Size of Empty class: " << sizeof(Empty) << " bytes" << std::endl;
std::cout << "Size of Simple class: " << sizeof(Simple) << " bytes" << std::endl;
std::cout << "Size of Inherited class: " << sizeof(Inherited) << " bytes" << std::endl;
std::cout << "Size of WithVirtual class: " << sizeof(WithVirtual) << " bytes" << std::endl;
return 0;
}
如何让类不被继承
C++11以后,使用final关键字来防止继承
class MyClass final {
public:
void show() {
std::cout << "This class cannot be inherited." << std::endl;
}
};
// 以下代码将导致编译错误
// class Derived : public MyClass {};
int main() {
MyClass obj;
obj.show();
return 0;
}
构造函数声明为privatehe或protected
class NonInheritable {
private:
NonInheritable() {}
friend class OnlyAllowed; // 如果你需要允许特定类访问构造函数
};
// 以下代码将导致编译错误
// class Derived : public NonInheritable {};
int main() {
// NonInheritable obj; // 这行代码也会导致编译错误,因为构造函数是私有的
return 0;
}
使用友元类和私有构造函数
class NonInheritable {
private:
NonInheritable() {}
friend class Factory; // 通过工厂类创建对象
public:
void show() {
std::cout << "This class can be instantiated but not inherited." << std::endl;
}
};
class Factory {
public:
static NonInheritable createInstance() {
return NonInheritable();
}
};
// 以下代码将导致编译错误
// class Derived : public NonInheritable {};
int main() {
NonInheritable obj = Factory::createInstance();
obj.show();
return 0;
}