C++ 学习 —— 03 - 类

目录

一、结构体的优化

二、类基础知识

2.1 什么是类?

2.2 类的访问限定符

2.3 类的声明

2.4 类的实例化

2.5 this 指针

三、类的默认成员函数

3.1 六大默认成员函数

3.2 构造函数

3.2.1 构造函数特点

3.2.2 构造函数的初始化列表

3.2.2.1 什么是初始化列表

3.2.2.2 初始化列表的优缺点

3.2.2.3 初始化列表的注意事项

3.3 析构函数

3.3.1 析构函数特点

3.3.2 顺序问题​

3.4 拷贝构造函数

3.4.1 拷贝构造函数特点

3.4.2 拷贝构造函数的触发条件

3.5 赋值运算符重载

四、类继承

4.1 基本概念

4.2 继承方式与权限

4.3 子类的默认成员函数

4.4 同名问题

4.4.1 非虚函数

4.4.2 虚函数

4.4.3 纯虚函数

4.5 多继承

4.5.1 基本概念

4.5.2 菱形继承(解决方案——虚继承)

五、杂项

5.1 特殊的静态成员

5.1.1 静态成员变量

5.1.2 静态成员函数

5.1.3 案例一

5.2 友元

5.2.1 友元函数(使用较少)

友元全局函数

友元成员函数

5.2.2 友元类(使用较多)

5.2.3 优缺点

5.3 委托构造函数

六、设计模式

6.1 创建型模式

6.1 单例模式


一、结构体的优化

#include <iostream>

using namespace std;

struct Student{
	int a = 10;
	int b = 5;
	void print() {  // 优化一:可以在此处定义函数
		cout << a << " " << b << endl;
	}
};

int main() {
	Student xiaoming; // 优化二:可以省略 struct
	xiaoming.print();
	return 0;
}

二、类基础知识

2.1 什么是类?

类可以理解为升级版的结构体。语法格式如下:

class className {

        // 类体:由成员变量和成员函数组成

};

2.2 类的访问限定符

具体访问权限详见 4.2。

口诀:私有化属性(变量),公有化行为(函数)。

我们在类中将变量私有化看起来没有什么问题,但是当我们实例化对象之后,我们如何获取和修改其变量呢?

#include <iostream>
using namespace std;

class Person {
private:
    int age;  // 私有化变量,外部无法直接访问

public:
    // 设置年龄(setter)
    void setAge(int newAge) {
        age = newAge;
    }

    // 获取年龄(getter)
    int getAge() {
        return age;
    }
    // 也可以获取和设置同时实现
    int setAndGetAge(int newAge) {
        age = newAge;
        return age;
    }
};

int main() {
    Person p;

    // ❌ 错误示例:无法直接访问私有成员
    // p.age = 30;  // 编译错误!

    // ✅ 正确示例:通过公有函数修改
    p.setAge(25);  // 修改私有变量
    cout << "年龄是:" << p.getAge() << endl;
}

其实我们在蓝图中已经用过很多次了,举个很简单的例子,我们在某个 Actor 蓝图类中调用 Get Actor Location / Set Actor Location 就是如此。

2.3 类的声明
  • 成员函数的声明和定义都在类中。
  • 成员函数的声明在类中(.h),成员函数的定义不在类中(.cpp)。

对于第二种声明方法,我们如何快速创建呢?按照如下方式操作即可。

然后编辑器会自动为我们创建 Character.h 和 Character.cpp。

如果我们有多个类的时候,请注意:

2.4 类的实例化

在 2.3 中,成员变量并没有分配空间。只有当类实例化时,才会分配空间。

#include<iostream>
using namespace std;
 
class Date {
public:
	void Init(int year, int month, int day)
	{
			_year = year;
			_month = month;
			_day = day;
	}
	int _year;
	int _month;
	int _day;
};
 
int main()
{
	Date a1;
	Date a2;
	Date a3;
    return 0;
}

但是现在就有一个问题了,上面我们说到了对于成员变量来说,若是类没有被实例化的话是不存在具体空间的,那在一个类中除了成员变量外,还有【成员函数】。仔细观察可以发现,我们在类中已经把成员函数定义出来了呀,那空间不是已经存在了吗。 此时我们再去实例化这个类的话,岂不是会造成重复定义了?

其实这跟类对象的存储方式有关,如下所示:

不同于成员变量,类中的成员函数存储在公共代码区,这也意味着每个对象所使用的函数是共同的,并不会因为每有一个对象就创建一个它自己的函数,它与其他对象所使用的函数是共同的。

2.5 this 指针

编译器会自动为我们添加(见右侧)。

这里的 this 可以写也可以不写,主要是为了演示一遍,这里的 this 指的就是对象 a1 的地址。

三、类的默认成员函数

3.1 六大默认成员函数

默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。需要注意的是这 6 个中最重要的是前 4 个,最后两个我们稍微了解⼀下即可。

3.2 构造函数

在C++中,构造函数是特殊的成员函数,这种函数的意义是在对象实例化时初始化对象,替代我们之前所写的 Init 函数,并且构造函数可以自动调用。

初始化逻辑:先走初始化列表,具体逻辑如下:

然后再走构造函数内部(里面有什么就走什么,不要想太多)。

注意:Weapon weapon; 是自定义类型成员,Weapon* weapon; 不是自定义类型成员。

3.2.1 构造函数特点

构造函数特点如下:

  • 构造函数名与类名相同,无返回值(也不用写 void)。
  • 构造函数分为无参构造函数和有参构造函数,并且可以重载(当为无参构造函数时,我们在类实例化时不能加括号,如下所示 )。

  • 如果类中没有显式定义构造函数,则C++编译器会自动生成⼀个无参的默认构造函数,⼀旦用户显式定义编译器将不再生成。
  • 无参构造函数、全缺省构造函数、编译器默认生成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在(因为调用时会引发歧义)。总结⼀下就是不传实参就可以调用的构造函数就叫默认构造函数。
  • 针对编译器默认构造 “不初始化内置类型” 的缺陷,C++11 允许在成员变量声明时直接赋予默认值(顺序为先走该默认值,然后再走初始化列表、构造函数等逻辑)

class Date {
private:
    // C++11补丁:内置类型声明时指定默认值
    int _year = 2023; 
    int _month = 10;
    int _day = 21;
};

Date d;
3.2.2 构造函数的初始化列表
3.2.2.1 什么是初始化列表

经过 3.2.1 的讲解,想必大家已经对构造函数比较熟悉了,可是大家是否遇到过,在构造函数后面跟了一个冒号的情况呢?

还是举上面我们写的例子,不过我们要扣一下字眼了。其实构造函数体内的语句只能将其称为【赋初值】,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。如下所示:

class Date
{
public:
	Date(int year = 2022, int month = 5, int day = 24)
	{
		_year = year;
		_year = 2023; //第二次赋值
        _year = 2024; //第三次赋值
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

现在,可否有一种方式进行初始化呢?即初始化列表初始化。

基本语法如下:

class Date
{
public:
    //构造函数: -->初始化列表初始化
	Date(int year = 2023, int month = 11, int day = 2)
		:_year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};

初始化列表是 C++ 中构造函数用来初始化其类成员的一种语法结构。它位于构造函数的参数列表之后,函数体之前,使用冒号 “:” 来引出,后跟一个或多个用逗号分隔的初始化项,每个初始化项由成员变量名和括号括起来的初始值组成。

3.2.2.2 初始化列表的优缺点

优点:

  • 可以提高程序运行效率。相较于在构造函数体内进行赋值操作,使用初始化列表的效率更高。
  • 方便且基本上可以替代括号初始化,可以接受任意长度,相对更加灵活。
  • 可以防止类型窄化,避免精度丢失的隐式类型转换。

缺点:

  • 初始化列表需要占用额外的存储空间。
  • 初始化列表只能在构造函数参数列表之后,所以在使用初始化列表时可能会导致代码可读性降低。
3.2.2.3 初始化列表的注意事项

3.3 析构函数
3.3.1 析构函数特点
  • 析构函数名是在类名前加上字符 ~,无参数无返回值。
  • ⼀个类只能有⼀个析构函数(因为析构函数没有形参,所以不能重载)。
  • 若未显式定义,系统会自动生成默认的析构函数。自动生成的析构函数对内置类型成员不做处理,自定义类型成员会调用它的析构函数。
  • 即使我们显式定义了析构函数,自定义类型成员也会调用它的析构函数,也就是说自定义类型成员无论什么情况都会自动调用它的析构函数。
class Stack
{
public:
	//构造函数
	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		assert(_a);
		_top = 0;
		_capacity = capacity;
	}
	//析构函数
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack st;
}

3.3.2 顺序问题

3.4 拷贝构造函数
3.4.1 拷贝构造函数特点
  • 拷贝构造函数是构造函数的一种重载(所以调用了拷贝构造函数也就不会调用构造函数)。
  • 拷贝构造函数的参数只有一个,且为本类对象的引用(一般常用 const 修饰)。

注意:如果拷贝构造函数的参数为本类对象的值,那么编译器会直接报错,因为在C++中规定了,类类型的传值传参要优先调用所传对象的拷贝构造函数,再去传给形参。因此会引发无穷递归调用。

#include<iostream>
#include<stdlib.h>
using namespace std;
class Date
{
public:
	Date(int year = 2000,int month = 1,int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
		cout << "Date(int year = 2000,int month = 1,int day = 1)" << endl << _year << ' ' << _month << ' ' << _day << endl;
	}
    //拷贝构造函数
    //这里const非必须写的,但最好加const
	Date(const Date& s)
	{
		_year = s._year;
		_month = s._month;
		_day = s._day;
		cout <<"Date(Date& s)" <<endl<< _year << ' ' << _month << ' ' << _day << endl;
	}
public:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date s1(2025, 1, 14);
	Date s2(s1); // 调用拷贝构造函数
    Date s3 = s1;// 调用拷贝构造函数(s3是新创建的对象) 与上面等价
    Date s4;
    s4 = s1; // 赋值运算符重载(两个已经存在的对象)
}
  • 若未显式定义,系统会自动生成默认的拷贝构造函数。自动生成的拷贝构造函数对于内置类型成员和自定义类型成员都会处理,执行的是“浅拷贝” 。
 
class Date
{
public:
	Date(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
		cout << "Date(int year = 2000,int month = 1,int day = 1)" << endl << _year << ' ' << _month << ' ' << _day << endl;
	}
	//拷贝构造函数
	/*Date(Date& s)
	{
		_year = s._year;
		_month = s._month;
		_day = s._day;
		cout << "Date(Date& s)" << endl << _year << ' ' << _month << ' ' << _day << endl;
	}*/
public:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date s1(2025, 1, 14);
	Date s2(s1);
	cout << endl;
	s2._year = 2000;
	cout << s1._year << ' ' << s1._month << ' ' << s1._day << endl;//输出2025 1 14
	cout << s2._year << ' ' << s2._month << ' ' << s2._day << endl;//输出2000 1 14
	//s2的值的改变并不会改变s1的值,间接说明所以说明s2与s1的空间是不同的
}
  • “浅拷贝”即直接复制成员变量的值。但当类中包含指针成员时,可能会导致多个对象指向同一块内存,容易引发内存泄漏或程序崩溃,这时需要用户显式定义拷贝构造函数避免这一情况(深拷贝)。

注意:如果未显式定义,系统会自动生成默认的拷贝构造函数。那么 st1 和 st2 中的 _array 指向的内存地址相同,由于析构函数的存在,会导致同一块动态内存析构两次,就会报错。

#include<iostream>
#include<stdlib.h>

using namespace std;

typedef int DataType;

class Stack
{
public:
	Stack(size_t capacity = 3)
	{
		cout << "Stack(size_t capacity = 3)" << endl;

		_array = new DataType[capacity];  
		if (_array == nullptr)
		{
			perror("new申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}

	Stack(const Stack& st)
	{
		_array = new DataType[st._capacity]; 
		if (_array == nullptr)
		{
			perror("new申请空间失败!!!");
			return;
		}

		for (int i = 0; i < st._size; ++i)
		{
			_array[i] = st._array[i];
		}

		_size = st._size;
		_capacity = st._capacity;
	}

	~Stack()
	{
		cout << "~Stack()" << endl;

		if (_array)
		{
			delete[] _array;  
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};

int main()
{
	Stack st1;

	// 拷贝构造
	Stack st2(st1);

	return 0;
}

3.4.2 拷贝构造函数的触发条件

3.5 赋值运算符重载

四、类继承

4.1 基本概念

继承是面向对象代码设计时 代码复用 的重要手段,它允许程序员在保持父类特性的基础上进行扩展,被继承的类称为父类或基类;继承的类称为子类或派生类。

基本语法如下:

class 子类名 继承符(:) 继承方式(public、protected、private) 父类名 {

}

注意:一般继承方式为 public。

4.2 继承方式与权限

在不涉及子类继承的情况下。

class Father {
public:
	int publicNum = 10;
protected:
	int protectedNum = 20;
private:
	int privateNum = 30;
};

int main() {
	Father f1;
	cout << f1.publicNum << endl; 
	// cout << f1.protectedNum << endl; // 无法访问
	// cout << f1.privateNum << endl;   // 无法访问
	return 0;
}

在涉及子类继承的情况下。

#include <iostream>
using namespace std;

// 父类
class Base {
public:
    int publicVar = 10; // 公有成员变量
protected:
    int protectedVar = 20; // 保护成员变量
private:
    int privateVar = 30; // 私有成员变量
};

// 子类,public继承
class DerivedPublic : public Base {
public:
    void test() {
        cout << publicVar << endl; // 可以访问
        cout << protectedVar << endl; // 可以访问
         //cout << privateVar << endl; // 无法访问
    }
};

// 子类,protected继承
class DerivedProtected : protected Base {
public:
    void test() {
        cout << publicVar << endl; // 可以访问
        cout << protectedVar << endl; // 可以访问
        // cout << privateVar << endl; // 无法访问
    }
};

// 子类,private继承
class DerivedPrivate : private Base {
public:
    void test() {
        cout << publicVar << endl; // 可以访问
        cout << protectedVar << endl; // 可以访问
        // cout << privateVar << endl; // 无法访问
    }
};

int main() {
    // 类外通过子类实例访问父类成员变量
    DerivedPublic dp;
    cout << dp.publicVar << endl; // 可以访问
     //cout << dp.protectedVar << endl; // 无法访问
     //cout << dp.privateVar << endl; // 无法访问

    DerivedProtected dpro;
    // cout << dpro.publicVar << endl; // 无法访问
    // cout << dpro.protectedVar << endl; // 无法访问
    // cout << dpro.privateVar << endl; // 无法访问

    DerivedPrivate dpriv;
    // cout << dpriv.publicVar << endl; // 无法访问
    // cout << dpriv.protectedVar << endl; // 无法访问
    // cout << dpriv.privateVar << endl; // 无法访问

    return 0;
}

总结如下:

  • 当访问限定符设为 public,全部可见。
  • 当访问限定符设为 protected,该类和子类可见。
  • 当访问限定符设为 private,该类可见。
  • class 默认权限是private,struct 默认权限是 public。
  • 经过子类继承后,父类成员在子类中的权限变为 min{访问限定符,继承方式}。(public > protected > private)

【补充】在 UE5 C++ 源代码中,ACharacter 类中胶囊体、Mesh、箭头组件等都设为 private,但我们创建子类后为什么也能看到呢?这是因为 UE5 反射系统的存在(暂不了解)。

4.3 子类的默认成员函数

注意:子类不会继承父类的构造函数(包括拷贝构造函数)、析构函数、友元关系等。

  • 子类在构造时,必须首先调用父类的默认构造函数。当父类没有默认构造函数时,则必须在子类的初始化列表中显式调用(利用委托构造函数)。
  • 子类在调用完析构函数后,也会调用父类的析构函数。
#include <iostream>
using namespace std;

class Person {
public:
	Person(){
		cout << "Person()" << endl;
	}
	~Person(){
		cout << "~Person()" << endl;
	}
};

class Student : public Person {
public:
	Student(){
		cout << "Student" << endl;
	}
	~Student(){
		cout << "~Student" << endl;
	}
};

int main() {
	Student s1;
    return 0;
}
Person()
Student
~Student
~Person()

D:\C++Study\MyStudy\x64\Debug\Day03.exe (进程 35640)已退出,代码为 0 (0x0)。
按任意键关闭此窗口. . .
#include <iostream>
using namespace std;

class Person {
public:
	Person(const string& name){
		cout << "Person()" << endl;
	}
	~Person(){
		cout << "~Person()" << endl;
	}
};

class Student : public Person {
public:
	Student(const string& name) : Person(name){
		cout << "Student" << endl;
	}
	~Student(){
		cout << "~Student" << endl;
	}
};

int main() {
	Student s1("xiaohong");
    return 0;
}
Person()
Student
~Student
~Person()

D:\C++Study\MyStudy\x64\Debug\Day03.exe (进程 35640)已退出,代码为 0 (0x0)。
按任意键关闭此窗口. . .

子类在拷贝构造时,必须首先调用父类的默认拷贝构造。当父类没有时。则必须在子类的初始化列表中显式调用(利用委托构造函数)。

#include <iostream>
using namespace std;

class Person {
public:
	// 构造函数
	Person(const string& name){
		cout << "Person()" << endl;
	}
	// 拷贝构造函数
	Person(const Person& p){
		cout << "Person(const Person& p)" << endl;
	}
	// 赋值运算符重载
	Person& operator=(const Person& p){
		cout << "Person& operator=(const Person& p)" << endl;
		if (this != &p)
		{
			_name = p._name;
		}
		return *this;
	}
	// 析构函数
	~Person(){
		cout << "~Person()" << endl;
	}
protected:
	string _name = "cp";
};

class Student : public Person {
public:
	// 构造函数 因为父类没有默认构造函数,所以必须在初始化列表中显式调用
	Student(const string& name, const int num)
		:Person(name){
		cout << "Student" << endl;
	}
	// 拷贝构造函数 因为父类没有默认拷贝函数,所以必须在初始化列表中显式调用
	Student(const Student& s)
		:Person(s){
		cout << "Student(const Student& s)" << endl;
	}
	// 赋值运算符重载 
	Student& operator=(const Student& s){
		cout << "Student& operator=(const Student& s)" << endl;
		if (this != &s){
			Person::operator=(s);
			_num = s._num;
		}
		return *this;
	}
	// 析构函数
	~Student(){
		cout << "~Student" << endl;
	}
protected:
	int _num = 0;
};

int main()
{
	Student s1("haha", 22);
	Student s2(s1);

	return 0;
}
Person()
Student
Person(const Person& p)
Student(const Student& s)
~Student
~Person()
~Student
~Person()

D:\C++Study\MyStudy\x64\Debug\Day03.exe (进程 41288)已退出,代码为 0 (0x0)。
按任意键关闭此窗口. . .

4.4 同名问题
4.4.1 非虚函数

在继承中,子类和父类是独立的作用域。那当 子类和父类有同样名字的成员(即使参数、返回值不相同) 时,优先使用谁的呢?

答案是子类优先使用。当然你也可以通过 :: 运算符访问父类的成员。

#include <iostream>
using namespace std;

class Person {
public:
    void Print() {
        cout << _name << endl;
    }
protected:
    string _name = "father";
};

class Student : public Person {
public:
    // 此处不是函数重载,因为两个类是独立的作用域
    // 此处不是函数重写,因为没有使用 virtual 关键字
    // 此处是函数重定义
    void Print() {
        cout << "我是:" << _name << endl;
    }
    string _name = "son";
};

int main() {
    Student s1;
    s1.Print(); // 我是:son
    s1.Person::Print(); // father
    return 0;
}
4.4.2 虚函数

在学习了 4.4.1 后,你可能会觉得,这是不是 C++ 中的多态呢?显然不是的。

多态分为静态多态(编译时多态)和动态多态(运行时多态)。

静态多态可以依靠函数重载、运算符重载等来实现,此处就不详细解释啦。

动态多态必须满足以下三个特性:

  • 存在有继承关系的父类和子类。
  • 子类对父类中某些方法进行重写。
  • 用一个父类指针(或引用)指向子类对象的时候,假如调用的是虚函数,会自动暂时将该指针转换为子类类型的指针(或引用)。

举个简单的例子:

#include <iostream>
using namespace std;
class Base {
public:
    void base_unique() { 
        cout << "base_unique method!\n"; 
    };
    // 基类定义的虚函数在所有子类中都是虚函数
    virtual void func() { 
        cout << "Base method!\n"; 
    };
};

class Derived : public Base {
public:
    // 此处我们可以省略 virtual,C++11 允许子类使用 override 关键字显式的注明该成员函数是重写的父类的虚函数(当然也可以省略)
    // 此处是函数重写
    void func() override { 
        cout << "Derived method!\n"; 
    };
};
int main()
{
    Base b1;
    Derived d1;
    b1.func(); // Base method!
    d1.func(); // Derived method! 如果子类不重写的话,会调用父类函数

    // 父类指针指向子类对象
    Base* p = new Derived();
    // 虚函数可以实现暂时的指针类型转换,由 Base 类型的指针转换为 Derived 类型的指针,然后再自动转换回来
    p->func(); // Derived method! 如果子类不重写的话,会调用父类函数
    // 由此可以看到这个类型转换只是暂时的
    p->base_unique(); // base_unique method!
}

总结如下:

  •  没有虚函数时,调用哪个函数,编译器在编译期就决定了。
  •  有虚函数时,调用哪个函数,运行时根据对象真实类型决定。
  •  多态的意义在于用父类接口操作子类对象,而不必关心子类的具体类型。

注意:其实我们在蓝图中也是用过 运行时多态的,比如我现在有 BP_Parent 和 BP_Child 两个蓝图类,两个蓝图类中都有 PrintName 这个函数(蓝图函数天然“虚”?),分别打印它们自己的名字。现在我又创建了一个 Actor 类,进行如下操作,你觉得会打印谁的名字呢(世界场景中为 BP_Child )?

答案是会打印子类的名字,因为 Get Actor of Class 返回父类的指针,世界场景中又为 BP_Child ,所以会调用子类的函数。

4.4.3 纯虚函数
  • 纯虚函数只需要声明,不需要定义。
  • 含有纯虚函数的类称为抽象类,抽象类不能实例化。
  • 对于父类中的纯虚函数,子类必须提供纯虚函数的个性化实现。
#include <iostream>
using namespace std;

// 定义一个抽象类 Shape
class Shape {
public:
    // 纯虚函数:只声明,不定义
    virtual void Draw() = 0;
};

// Circle 继承自 Shape,必须实现纯虚函数 Draw
class Circle : public Shape {
public:
    void Draw() override {
        cout << "Drawing a Circle.\n";
    }
};

// Rectangle 继承自 Shape,也必须实现纯虚函数 Draw
class Rectangle : public Shape {
public:
    void Draw() override {
        cout << "Drawing a Rectangle.\n";
    }
};

int main() {
    // Shape s;  // ❌ 错误!抽象类不能实例化
    Shape* p1 = new Circle();
    Shape* p2 = new Rectangle();

    // 调用多态函数
    p1->Draw();  // 输出:Drawing a Circle.
    p2->Draw();  // 输出:Drawing a Rectangle.

    return 0;
}

补充:

https://blog.csdn.net/weixin_43940314/article/details/125499272

4.5 多继承
4.5.1 基本概念

基本语法如下:

class 子类名 继承符(:) 继承方式1 父类名, 继承方式2 父类名,...... {

}

#include <iostream>

using namespace std;

class Base1 {
public:
    int num = 10;
    int numA = 1;
};

class Base2 {
public:
    int num = 20;
    int numB = 2;
};

class Son : public Base1, public Base2 {
public:
    Son() {
        //当父类中出现同名成员,需要加作用域区分
        Base1::num = 4;
        Base2::num = 10;
    }
};

int main() {
    Son s1;
    cout << s1.numA << " " << s1.numB << endl; // 1 2

    //当父类中出现同名成员,需要加作用域区分
    // cout << s1.num << endl; // 报错
    cout << s1.Base1::num << endl; // 4
    cout << s1.Base2::num << endl; // 10
    return 0;
}
4.5.2 菱形继承(解决方案——虚继承)

两个派生类继承同一个基类,又有某个类同时继承着两个派生类,这种继承被称为菱形继承。

#include <iostream>

using namespace std;

class Animal { 
public:
	int a,b,c,d,e,f = 10;
	Animal() {
		cout << "Animal构造函数" << endl;
	}
	~Animal() {
		cout << "Animal析构函数" << endl;
	}
};

class Sheep : public Animal {
public:
	Sheep() {
		cout << "Sheep构造函数" << endl;
	}
	~Sheep() {
		cout << "Sheep析构函数" << endl;
	}
};

class Camel : public Animal {
public:
	Camel() {
		cout << "Camel构造函数" << endl;
	}
	~Camel() {
		cout << "Camel析构函数" << endl;
	}
};

class Alpca : public Sheep, public Camel {
public:
	Alpca() {
		cout << "Alpca构造函数" << endl;
	}
	~Alpca() {
		cout << "Alpca析构函数" << endl;
	}
};

int main() {
	Alpca a1;

	//当父类中出现同名成员,需要加作用域区分
	cout << a1.Sheep::f << endl; // 10
	cout << a1.Camel::f << endl; // 10

	cout << sizeof(Animal) << endl; // 24
	cout << sizeof(Sheep) << endl;  // 24   
	cout << sizeof(Camel) << endl;  // 24   
	cout << sizeof(Alpca) << endl;  // 48  

	return 0;
}


Animal构造函数
Sheep构造函数
Animal构造函数
Camel构造函数
Alpca构造函数
10
10
24
24
24
48
Alpca析构函数
Camel析构函数
Animal析构函数
Sheep析构函数
Animal析构函数

D:\C++Study\MyStudy\x64\Debug\Day03.exe (进程 24528)已退出,代码为 0 (0x0)。
按任意键关闭此窗口. . .
  • 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
  • 利用虚继承可以解决菱形继承问题
#include <iostream>

using namespace std;

class Animal { // 此时 Animal 变为虚基类
public:
	int a,b,c,d,e,f = 10;
	Animal() {
		cout << "Animal构造函数" << endl;
	}
	~Animal() {
		cout << "Animal析构函数" << endl;
	}
};

// 加上关键字 virtual,变为虚继承
class Sheep : virtual public Animal {
public:
	Sheep() {
		cout << "Sheep构造函数" << endl;
	}
	~Sheep() {
		cout << "Sheep析构函数" << endl;
	}
};

class Camel : virtual public Animal {
public:
	Camel() {
		cout << "Camel构造函数" << endl;
	}
	~Camel() {
		cout << "Camel析构函数" << endl;
	}
};

class Alpca : public Sheep, public Camel {
public:
	Alpca() {
		cout << "Alpca构造函数" << endl;
	}
	~Alpca() {
		cout << "Alpca析构函数" << endl;
	}
};

int main() {
	Alpca a1;

	cout << a1.f << endl; // 此时可以直接访问

	cout << sizeof(Animal) << endl; // 24
	cout << sizeof(Sheep) << endl;  // 32   Animal 成员变量 24 字节 + 虚表指针 8 字节
	cout << sizeof(Camel) << endl;  // 32   Animal 成员变量 24 字节 + 虚表指针 8 字节
	cout << sizeof(Alpca) << endl;  // 40   Animal 成员变量 24 字节 + 虚表指针(用于Sheep) 8 字节 + 虚表指针(用于Camel) 8 字节

	return 0;
}


Animal构造函数
Sheep构造函数
Camel构造函数
Alpca构造函数
10
24
32
32
40
Alpca析构函数
Camel析构函数
Sheep析构函数
Animal析构函数

D:\C++Study\MyStudy\x64\Debug\Day03.exe (进程 31696)已退出,代码为 0 (0x0)。
按任意键关闭此窗口. . .

五、杂项

5.1 特殊的静态成员

特点:

  • 属于整个类,而不是某个对象。

  • 所有对象共享一份静态成员(在某一对象修改静态成员,所有对象都会被修改)。

  • 可以不通过对象实例,直接用类名访问。

5.1.1 静态成员变量

静态成员变量与普通成员变量不同,它不能在类内初始化(除非声明为静态常量成员)。

class MyClass {
public:
    static int x = 10; // 错误
    static int y; 
    static const int z = 10; // 静态常量成员可以类内初始化
};
int MyClass::y = 10; // 类外初始化
5.1.2 静态成员函数

静态成员函数与普通成员函数不同,它只能访问静态成员,因为它没有 this 指针。

#include <iostream>
using namespace std;

class MyClass {
public:
    static int x; // 声明静态成员变量

    static void init(int value) { // 静态成员函数
        x = value;
    }
};

int MyClass::x = 0; // 定义静态成员变量

int main() {
    MyClass::init(10); // 调用静态成员函数进行初始化
    cout << MyClass::x << endl;
    return 0;
}

5.1.3 案例一
class A {
public:
    int x = 10;          // 普通成员变量
    void print() {        // 普通成员函数
        cout << x << endl;
    }
};

int main() {
    // ❌ 不能直接用类名访问普通成员
    // cout << A::x;       // 错误!
    // A::print();         // 错误!

    // ✅ 需要实例化对象
    A a;
    cout << a.x << endl;   // 正确
    a.print();             // 正确
}
class B {
public:
    static int count;        // 静态成员变量声明
    static void showCount() {  // 静态成员函数
        cout << count << endl;
    }
};

// 静态成员变量定义(必须在类外定义一次)
int B::count = 100;

int main() {
    // ✅ 不需要创建对象,直接通过类名访问
    cout << B::count << endl;  
    B::showCount();

    // ✅ 当然也可以通过对象访问
    B b;
    cout << b.count << endl;  
    b.showCount();
}

5.2 友元

5.2.1 友元函数(使用较少)
友元全局函数

特点如下:

  • 友元函数在类的声明中使用关键字 friend 来标识。
  • 友元函数的声明只是赋予其访问权限,并不会将其变为该类的成员函数。它仍然是一个全局函数。(所以友元函数没有 this 指针,可以像普通函数一样调用)。
  • 友元函数的特殊性在于可以访问类的私有成员和保护成员
  • 推荐将友元函数的实现放在类的外部,保持类的简洁性和可维护性。
class Rectangle {
private:
    int width, height;
public:
    Rectangle(int w, int h) : width(w), height(h) {}
    friend int calculateArea(Rectangle rect);  // 声明友元函数
};
 
int calculateArea(Rectangle rect) {
    return rect.width * rect.height;  // 可以访问私有成员
}
  • 友元函数可以同时作为多个类的友元,从而实现跨类的数据访问。
class ClassB;  // 前向声明
 
class ClassA {
private:
    int valueA;
public:
    ClassA(int v) : valueA(v) {}
    friend int compare(ClassA a, ClassB b);
};
 
class ClassB {
private:
    int valueB;
public:
    ClassB(int v) : valueB(v) {}
    friend int compare(ClassA a, ClassB b);
};
 
int compare(ClassA a, ClassB b) {
    return a.valueA - b.valueB;  // 同时访问两个类的私有成员
}
    友元成员函数

    特点与上方类似,不做赘述。

    但注意易错点,如下:

    如果我们把类 Community 声明在前面,friend void DecorationDesigner::work() 就会找不到 void DecorationDesigner::work() 在哪里,即使你在前面添加 class DecorationDesigner 也是不行的,因为编译器只知道有这个类,里面具体有什么不知道。所以下方是错误代码!!!

    #include <iostream>
    using namespace std;
    
    class DecorationDesigner; 
    
    class Community
    {
        friend void DecorationDesigner::work();  
    public:
        Community()
        {
            PublicAreas = "公共区域";
            home = "你家";
        }
        string PublicAreas;
    private:
        string home;
    };
    
    
    class DecorationDesigner
    {
    public:
        void work() {
            Community _community;
            cout << "装修设计师类正在访问:" << _community.PublicAreas << endl;
            cout << endl;
            cout << "装修设计师类正在访问:" << _community.home << endl;
        }
    };
    
    int main()
    {
        DecorationDesigner designer;
        designer.work();
        return 0;
    }

    那么怎么办呢?

    #include <iostream>
    using namespace std;
    
    class DecorationDesigner
    {
    public:
        void work();
    };
    
    class Community
    {
        friend void DecorationDesigner::work();
    public:
        Community()
        {
            PublicAreas = "公共区域";
            home = "你家";
        }
        string PublicAreas;
    private:
        string home;
    };
    
    void DecorationDesigner::work() {
        Community _community;
        cout << "装修设计师类正在访问:" << _community.PublicAreas << endl;
        cout << endl;
        cout << "装修设计师类正在访问:" << _community.home << endl;
    }
    
    
    int main()
    {
        DecorationDesigner designer;
        designer.work();
        return 0;
    }

    5.2.2 友元类(使用较多)

    特点与上方类似,不做赘述。

    感觉友元类和上方友元成员函数比起来就方便多了,直接前置声明即可。

    #include <iostream>
    #include <string>
    using namespace std;
    
    // 前置类的声明
    class DecorationDesigner;
    
    class Community
    {
        friend class DecorationDesigner;
    
    public:
        Community()
        {
            PublicAreas = "公共区域";
            home = "你家";
        }
        string PublicAreas;  
    private:
        string home; 
    };
    
    class DecorationDesigner
    {
    public:
        DecorationDesigner()
        {
        }
        void work()
        {
            cout << "装修设计师类正在访问," << _community.PublicAreas << endl;
            cout << endl;
            cout << "装修设计师类正在访问," << _community.home << endl;
        }
    private:
        Community _community;
    };
    
    int main()
    {
        DecorationDesigner designer;
        designer.work();
        return 0;
    }

    5.2.3 优缺点

    5.3 委托构造函数

    在一个类中,如果某个构造函数把对象的初始化工作“委托”给该类的另一个构造函数来完成,那么发起调用的那个就是委托构造函数。

    #include <iostream>
    #include <string>
    using namespace std;
    
    class MyClass {
        int a;
        int b;
    
    public:
        // 基础构造函数
        MyClass(int x) : a(0), b(0) {
            a = x;
            cout << "xiaoming" << endl;
        }
        // 委托构造函数:将初始化工作委托给上面的构造函数
        MyClass() : MyClass(10) {
            cout << "xiaohong";
        }
    };
    
    int main() {
        MyClass t1; 
        return 0;
    }
    xiaoming
    xiaohong
    D:\C++Study\MyStudy\x64\Debug\Day02.exe (进程 83488)已退出,代码为 0 (0x0)。
    按任意键关闭此窗口. . .

    六、设计模式

    6.1 创建型模式
    6.1 单例模式

    https://blog.csdn.net/qq_68194402/article/details/141405614

    目标:保证一个类仅有一个实例存在,同时提供能对该实例访问的全局方法。

    特点:

    • 私有化构造函数和静态成员变量,防止外部直接创建类的实例。
    • 提供一个公共静态方法(通常命名为 getInstance()),以确保所有调用者都能获取相同实例。
    • 类加载时创建实例(饿汉式)和首次调用时创建实例(懒汉式)。

    饿汉式如下:

    特点:

    • 类加载时创建实例。
    • 不存在多线程安全问题。
    • 但无论是否用到该对象,都会创建,可能浪费内存。
    class GameConfig {
    private:
        GameConfig() {};
        static GameConfig* m_instance;
        
        // 禁用
        GameConfig(const GameConfig&) = delete;
        GameConfig& operator=(const GameConfig&) = delete;
    
    public:
        static GameConfig* getInstance() {
            return m_instance;
        }
    };
    
    GameConfig* GameConfig::m_instance = new GameConfig();
    
    int main() {
    
        GameConfig* config1 = GameConfig::getInstance();
        GameConfig* config2 = GameConfig::getInstance();
    
        return 0;
    }

    懒汉式如下:

    特点:

    • 首次调用时创建实例。
    • 存在多线程安全问题,需手动加锁。
    • 节省内存资源。
    class GameConfig {
    private:
        GameConfig() {};
        static GameConfig* m_instance;
        
        // 禁用
        GameConfig(const GameConfig&) = delete;
        GameConfig& operator=(const GameConfig&) = delete;
    
    public:
        static GameConfig* getInstance() {
            if (m_instance == nullptr) {
                m_instance = new GameConfig();
            }
            return m_instance;
        }
    };
    
    GameConfig* GameConfig::m_instance = nullptr;
    
    int main() {
    
        GameConfig* config1 = GameConfig::getInstance();
        GameConfig* config2 = GameConfig::getInstance();
    
        return 0;
    }

    局部静态变量懒汉式如下:(C++11 推荐)

    特点:

    • 首次调用时创建实例。
    • 不存在多线程安全问题。
    • 节省内存资源。
    • 如果单例类的实例通过 new 创建,而在程序结束时没有释放内存,可能导致内存泄漏。而使用局部静态变量的方法在程序结束时会自动释放内存。
    #include <iostream>
    
    using namespace std;
    
    class GameConfig {
    private:
        GameConfig() {}
        
        // 禁用
        GameConfig(const GameConfig&) = delete;
        GameConfig& operator=(const GameConfig&) = delete;
    public:
        static GameConfig& getInstance() {
            // 局部静态变量(延长生命周期)
            static GameConfig instance;  // C++11 起线程安全
            return instance;
        }
    };
    
    int main() {
        auto& a = GameConfig::getInstance();  // 第一次创建
        auto& b = GameConfig::getInstance();  // 复用同一个实例
        return 0;
    }
    

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值