面向对象
封装、继承、多态
类
静态成员变量的初始化时机:静态成员变量在程序开始执行时进行初始化,而不是在类对象创建时。也就是说,静态成员变量只会在程序开始执行时进行初始化,而不会在类对象创建时进行初始化。
成员的类型
公有(public)、私有(private)、保护(protect),可以是数据,亦可以是函数(方法)。
=delete
在某些默认函数后追加会禁用该函数的功能,很好的隔离一些操作带了的影响。
explict
使得传入参数不能隐式转换。
匿名对象:Name()
如果是一个仿函数,则匿名函数对象 Name()()
匿名的特点就是执行完毕该段代码,该对象内存空间就会释放。
成员函数
编译器会默认给一个类添加:构造函数、析构函数、拷贝构造、拷贝赋值运算符重载(=,编译器提供的是浅拷贝)、移动构造(&&)(c++11)、移动赋值运算符重新载(c++11)。
构造函数
构造函数是一种特殊的成员函数,它的主要目的是在创建对象时初始化对象。当使用new操作符创建对象、使用变量声明对象(在栈上)或者通过返回对象的方式调用函数时,构造函数会被自动调用。例子:complex(double r=0, double i=0):re(r), im(i){};
一般通过 re(r), im(i)
的方式给成员变量赋值。
- 拷贝构造(复制构造):深拷贝
complex(const complex& c);
和浅拷贝complex(complex& c)
。编译器有默认的拷贝构造(按位赋值),然而带指针的类,比较特殊,需要我们动态分配内存,避免浅拷贝。 - 移动构造:
complex&&(complex p);
。移动构造函数接受一个对其自身类型的右值引用。移动构造函数会被标记为noexcept,以表明它不会抛出异常。该函数内部的实现通常意味着交换指针、文件句柄等,而不是复制它们指向的内容
注意:没有为类定义任何构造函数,编译器会提供一个默认的构造函数,该构造函数不接受任何参数并且不进行任何初始化操作(对于内置类型成员变量,其值将是未定义的)。但是,一旦定义了任何构造函数(无论是自定义的还是编译器生成的默认构造函数被删除的情况),编译器就不会再自动生成默认构造函数。
析构函数
析构函数是另一种特殊的成员函数,它在对象生命周期结束时被自动调用,用于执行清理工作,如释放分配的内存(free/delect)、关闭文件句柄等。析构函数的名称是在类名前加上波浪号(~)。例如:~complex();
注意:编译器同样给析构函数提供了默认的模板。
object 的生命周期
stack object 是会在作用域(scope)结束前调用清理(析构),所以也称auto。
static local object 是在离开当前作用域对象依然存在,会在程序结束时清理(析构)。
global object 是在main函数前就存在(构造),析构是在main函数执行后结束(析构)。
new
和 delete
new 是在堆上申请内存空间,然后进行构造。delete 是先进行析构,然后回收堆上的内存空间。
new[]
搭配 delete[]
使用。如果不搭,会导致只调用一次析构,从而内存泄漏。因为 new[]
申请的是连续的内存空间。
实例:
class ob:{}
ob* q = new ob[3];
delete[] q;
静态成员
静态成员变量
- 该类的所有实例化对象共享该变量数据。
- 变量在编译阶段完成内存分配。(数据区)
- 在类内部定义,类外部必须初始化。
class Name{
public:
static int num;
};
// 类名访问,也是初始化方式
int Name::num = 10;
// 对象访问,当某一对象修改数据后,所有该类的实例化对象该数据都会变化。
Name n = new Name()
n.num = 11;
静态成员函数
- 静态成员函数只能访问静态成员变量或调用静态成员函数。
- 类的所有实例化对象,共享该成员函数。
对象模型与this指针
对象模型
类的成员函数和成员变量是分开存储。只有非静态成员在类的实例化对象上。
class Person
{
public:
int a;
static int b;
void func(){};
static void func1(){};
};
Person p; // sizeof的结果是4
空对象占内存大小为1字节。这是编译器为了区分对象的内存位置。因此每一个空对象都会有一个这样的空间。
class Person
{
};
Person p; // sizeof的结果是1个字节
this指针
非静态成员函数只会有产生一份(代码段),实例化对象通过this指针区分是调用该函数的对象。即this指针指向被调用成员函数所属于的对象。this指针是隐含在成员函数中。this指针是一个指针常量。
class Name{
public:
int m_num;
Name(int num){
// 区分同名
this->m_num = num;
}
// 如果不是引用,每次调用都会重新构造(拷贝)一个对象。
Name& add(int t){
this->m_num += t;
return *this;
}
};
Name n(1);
n.add(10).add(10); // 链式调用
- 解决名称冲突。
- 返回调用该成员函数的当前对象。可以实现链式编程。
注意 : 因为this指针的存在,空指针也是可以调用成员函数的,当该成员函数使用了成员属性时需要注意,因为空指针无法访问到该成员会报错。
常函数是在成员函数添加 const
修饰 void func() const {}
不允许修改成员属性 ,限制了 this
。在成员变量添加 mutable
修饰 mutable int a;
可以让 a
成员在该函数内或常对象内可以修改。
常对象即在对象前加 const
,特殊点在于该对象不可调用非常函数。因此常对象只能调用常函数。
友元
友元目的是为了另外一个类访问当前类的私有属性。友元是一种特殊东西,它不是类的成员函数,但可访问类的所有私有成员和保护成员。这种访问权限是通过在类定义中显式声明该函数为友元来授予的。友元函数通常用于实现那些需要访问类的私有成员但又不适合作为类成员函数的情况,比如操作符重载函数或两个类之间的紧密协作。
实现方式:
- 全局函数做友元。
- 类做友元。
- 类的成员函数做友元。
实例:
class Name{
// 全局函数做友元
friend void func(Name& n);
// 类做友元
friend class Friend;
// 类成员函数访问私有成员
friend void Friend::func2();
public:
int m_num;
private:
// 私有成员
int m_p_num;
};
class Friend{
public:
void func1();
void func2();
Name* n;
};
Friend::Friend(){
n = new Name;
};
Friend::~Friend(){
delete n;
}
void Friend::func1(){
// 作为友元类访问到了私有属性
this->n->m_p_num = 11;
}
void Friend::func2(){
// 当类不做友元的时候该函数也能访问私有。
}
void func(Name& n){
// 全局函数可以访问私有属性
n.m_p_num = 10;
}
特殊的类
类像指针(仿指针/智能指针)
仿指针(也称为智能指针)的主要目的是自动管理动态分配的内存,以避免内存泄漏和悬挂指针等问题。智能指针通过封装裸指针(raw pointer)并提供自动的 内存管理功能(如自动释放内存)来实现这一目标。 unique_ptr
风格的仿指针实现示例。
#include <iostream>
template<typename T>
class UniquePtr {
private:
T* ptr;
// 禁止拷贝构造函数和拷贝赋值运算符
UniquePtr(const UniquePtr<T>&) = delete;
UniquePtr<T>& operator=(const UniquePtr<T>&) = delete;
public:
// 默认构造函数
UniquePtr() : ptr(nullptr) {}
// 构造函数,接受一个裸指针
explicit UniquePtr(T* p) : ptr(p) {}
// 移动赋值运算符
UniquePtr<T>& operator=(UniquePtr<T>&& other) noexcept {
if (this != &other) {
delete ptr;
ptr = other.ptr;
other.ptr = nullptr;
}
return *this;
}
// 析构函数
~UniquePtr() {
delete ptr;
}
// 访问裸指针
T* get() const {
return ptr;
}
// 解引用
T& operator*() const {
assert(ptr != nullptr);
return *ptr;
}
...
}
// 使用示例
int main() {
UniquePtr<int> ptr1(new int(10));
UniquePtr<int> ptr2 = std::move(ptr1); // 移动语义
std::cout << *ptr2 << std::endl; // 输出: 10
// 此时ptr1已经被置为nullptr,尝试访问会导致未定义行为
// std::cout << *ptr1 << std::endl; // 注释掉这行代码
return 0;
}
类像函数(仿函数)
C++中的仿函数(Functor)是一种可以像函数一样被调用的对象。它通常是通过重载 operator()
来实现的,这使得对象可以像函数一样被调用。
#include <iostream>
class Sum {
public:
// 定义一个带有两个int参数的operator()
int operator()(int a, int b) const {
return a + b;
}
};
int main() {
Sum sum;
// 直接像函数一样调用对象
std::cout << "Sum(5, 3) = " << sum(5, 3) << std::endl;
return 0;
}
多态
多态是泛型编程的一种思想,即同样的代码实现不同的功能。C++实现多态的方式有函数重载,运算符重载和虚函数等。前两者为静态多态,在编译时绑定函数的内存地址,后者为动态多态,在函数运行时绑定函数的内存地址。
函数重载
函数重载的几个关键点:
- 函数名相同 :重载的函数必须具有相同的函数名。
- 参数列表不同 :这包括参数的类型、顺序或个数不同。仅返回类型不同不足以区分两个重载函数。
- 作用域相同 :重载函数必须在同一个作用域内(如同一个类、同一个命名空间等)。
- 可以返回不同的类型 :虽然函数重载允许函数返回不同的类型,但决定函数重载的是参数列表,而不是返回类型。
函数重载的优点包括:
- 提高了代码的可读性 :通过为执行相似任务但参数不同的函数使用相同的名称,可以使代码更加清晰易读。
- 提高了代码的灵活性 :通过重载函数,可以根据传入参数的不同自动选择执行相应的函数体,增加了代码的灵活性。
#include <iostream>
using namespace std;
// 函数重载示例
void print(int i) {
cout << "Printing int: " << i << endl;
}
void print(double f) {
cout << "Printing float: " << f << endl;
}
void print(const char* c) {
cout << "Printing character: " << c << endl;
}
int main() {
print(7); // 调用 print(int)
print(7.7); // 调用 print(double)
print("Hello"); // 调用 print(const char*)
return 0;
}
运算符重载
运算符重载可以是类的成员函数,亦可以是全局函数。
- 不能重载的运算符 :并非所有运算符都可以被重载。例如,
.
(成员访问运算符)、.*
(成员指针访问运算符)、::
(作用域解析运算符)、sizeof
(类型大小运算符)、typeid
(类型识别运算符)、?:
(条件运算符)、#
(预处理符)等就不能被重载。 - 重载的运算符保持其原有的优先级和结合性 :当你重载一个运算符时,你只是改变了它作用于你的类对象时的行为,而不是改变了它的优先级或结合性。
- 重载的运算符可以是成员函数或友元函数 :大多数运算符都可以作为成员函数或友元函数来重载。但是,有一些运算符(如赋值运算符
=
、下标运算符[]
等)通常作为成员函数重载,而流插入运算符<<
和流提取运算符>>
则通常作为友元函数重载。 - 运算符函数不能有默认参数 :由于运算符函数的参数是隐式传递的,因此它们不能具有默认参数。
例如:
#include <iostream>
class Point {
public:
int x, y;
// 构造函数
Point(int a = 0, int b = 0) : x(a), y(b) {}
// 重载加法运算符
Point operator+(const Point& p) {
Point temp;
temp.x = x + p.x;
temp.y = y + p.y;
return temp;
}
};
int main() {
Point p1(2, 3), p2(5, 6), p3;
p3 = p1 + p2; // 使用重载的加法运算符
std::cout << "The sum of points is: (" << p3.x << ", " << p3.y << ")" << std::endl;
return 0;
}
<<
左移运算符重载、++a
和 a++
的重载(前置自增、后置自增,这里后置自增需要利用函数占位参数)、赋值运算符重载(是为了避免浅拷贝出现的操作堆上内存的问题(析构),注意=的链式调用)
虚函数
多态的一种实现是可以通过虚函数, vitual
关键字。纯虚函数的定义需要加入 =0
如下。包含有虚函数的类称为抽象类。成员函数都是纯虚函数的类称为接口类。(发生继承和虚函数重写)。虚函数的多态是父类通过指针或引用指向子类的对象。
class OB: {
public:
virtual void name() const = 0; // 纯虚函数
virtual void name(); // 虚函数
void name();
}
class Obs: public Ob{
public:
void name() // 子类重写
}
虚函数表与虚函数表指针
虚函数表 vfb
是一个指针数组,存放虚函数的函数指针。虚函数表指针 vfptr
是当类初始化一个实例时,该实例带有的一个访问虚函数表的指针。存在虚函数的类或继承了有虚函数的类都会存有虚函数表。虚函数表与虚函数表指针的示意图,图中的是多重继承的方式 a->b->c。
- 当子类重写了父类的虚函数时,子类的虚函数表会将该函数的地址替换(父类也存在这样一个地址但不同)到虚函数表的对应位置。
- 当父类的指针或引用调用该子类对象的方法时(发生多态),就会从子类对象的虚函数表中寻找到上面替换的函数地址。
注意:在多继承时,该类的实例对象有多个虚表指针。
继承
继承是面向对象编程的特点之一。无论在哪种继承方式下,子类都无法访问父类中访问权限为 private 的成员,友元成员不会被继承。子类可以通过添加作用域 父类类名::
访问或调用父类的属性和方法。
继承方式
公有(public):在父类中,访问权限为 public、protected 的属性,继承到子类后,访问权限不变。
保护(protect):在父类中,访问权限为 public、protected 的属性,继承到子类后,访问权限均为protected,不能被派生类的对象直接访问,但是可以在派生类的内部和派生类的子类中进行访问。
私有(private):在父类中,访问权限为 public、protected 的属性,继承到子类后,访问权限均为private。
子类的构造与析构、覆盖
构造的过程:子类调用父类的构造,在进行子类的构造
析构的过程:子类先调用析构,在析构父类。一般的父类的析构函数是一个虚函数。
处理父类与子类的同名成员的方式是覆盖。数据同名:在类内部,子类覆盖父类。函数同名:子类覆盖父类,这没重载的关系。根据类名作用域区分。
多继承和多层继承
多继承是同时拥有多个父类。在此有虚继承解决多继承访问不明确的问题,但是内存开销大。a->b,c b,c->d
多层继承是a->b->c,这种每层只有一个父类,但存在父类的父类的形式。
协变
协变(covariant)是指派生类(子类)中的返回类型可以是基类(父类)中返回类型的子类型。是一种语法特性。
class ob{
public:
virtual ob* name(int old, char* name){
return new ob(*this);
}; // 虚函数
}
class obs: public ob{
public:
obs* name(int old, char* name){
return new obs(*this);
} // 子类重写
}
菱形继承
- 当两个父类拥有相同的属性时,需要通过添加作用域区分
- 导致数据存在两份,空间浪费,并产生二义性。
解决方案通过虚继承解决。在中间的两个基类继承前添加 virtual
关键字,称为虚基类。原理:这两个类的虚基类指针 vbptr
会记录指向同名数据的地址的偏移量,通过该指针的当前地址和偏移量即可访问到基类属性,该属性在内存中只存在一份,因此在访问时无需加入作用域。