const
关键字
一、const
关键字
-
修饰成员变量
-
常成员变量:必须通过构造函数的初始化列表进行初始化,且初始化后不可修改。
-
示例:
class Student { private: const int age; // 常成员变量 public: Student(string name, int age) : age(age) { ... }};
-
-
修饰成员函数
-
常成员函数:
-
不能修改类的成员变量(编译时检查)。
-
可以与普通成员函数构成重载,常对象只能调用常成员函数。
-
-
示例:
class Student { public:const void show() { ... } // 版本1:返回值为const voidconst void show() const { ... } // 版本2:返回值为const void,且是const成员函数void show() const { ... } // 版本3:返回值为void,且是const成员函数void show() { ... } // 版本4:普通成员函数private:std::string name;int age; };const Student stu("Alice", 20); stu.show(); // 调用常成员函数版本
-
-
修饰对象
-
常对象:
-
对象初始化后不可修改成员变量。
-
只能调用常成员函数。
-
-
示例:
const Student stu("Bob", 18); // stu.setAge(19); // 错误:常对象不可修改成员变量 stu.show(); // 正确:调用常成员函数
-
二、static
与const
修饰构造函数和析构函数
构造函数
static
修饰:
构造函数用于初始化对象实例,而
static
成员属于类层级,与对象实例化机制冲突。禁止:C++语法规定构造函数不能是
static
。
const
修饰:
构造函数需要修改对象状态(初始化成员变量),与
const
的不可修改语义矛盾。禁止:构造函数不能声明为
const
。析构函数
static
修饰:
析构函数与对象生命周期绑定,而
static
成员属于类层级,无法处理具体实例的资源释放。禁止:析构函数不能是
static
。
const
修饰:
析构函数需要释放资源(如动态内存),而
const
限制成员变量的修改。禁止:析构函数不能声明为
const
。
三、static
与const
修饰成员变量和函数
-
成员变量
-
static const
成员变量:-
允许同时使用,表示类层级的常量(所有对象共享同一值)。
-
必须在类外初始化(C++11后支持类内初始化)。
-
示例:
class Math { public:static const double PI = 3.14159; // 类内初始化(C++11) };
-
-
-
成员函数
-
static
和const
同时修饰:-
无意义:
static
成员函数不关联对象实例(无this
指针),而const
要求不修改对象状态。 -
禁止:C++语法不允许
static
成员函数声明为const
。
-
-
四、关键对比与注意事项
场景 | const 修饰 | static 修饰 |
---|---|---|
成员变量 | 必须初始化,不可修改 | 类层级变量,所有对象共享 |
成员函数 | 不能修改成员变量,支持重载 | 无this 指针,不能访问非静态成员 |
对象 | 只能调用常成员函数 | 不适用(static 不修饰对象) |
构造函数/析构函数 | 禁止(需修改对象状态) | 禁止(与对象生命周期绑定) |
友元(Friend)
一、 友元(Friend)
1. 核心概念
-
友元函数:允许外部普通函数访问类的私有成员,需在类内用
friend
声明。 -
友元成员函数:允许另一个类的某个成员函数访问当前类的私有成员,需在类内声明该成员函数为友元。
-
友元类:允许另一个类的所有成员函数访问当前类的私有成员,需在类内声明友元类。
2. 特点
-
单向性:友元关系不可逆(若类A是类B的友元,类B不自动成为类A的友元)。
-
无继承性:基类的友元不是派生类的友元。
-
无传递性:若类A是类B的友元,类B是类C的友元,类A不自动成为类C的友元。
3. 应用场景
-
运算符重载(如
operator<<
用于输出)。 -
跨类协作(如矩阵类与向量类共享数据)。
-
工具函数需要访问私有成员时(如调试函数)。
4. 示例代码
#include <iostream>
#include <string>
using namespace std;// 友元函数
class Student {
private:string name;int age;
public:Student(const string &n, int a) : name(n), age(a) {}friend void printStudent(const Student& s); // 声明友元函数
};void printStudent(const Student& s) {cout << "Name: " << s.name << ", Age: " << s.age << endl; // 直接访问私有成员
}// 友元类
class Display {
public:void show(const Student& s);
};class Student {
private:string name;int age;
public:Student(const string &n, int a) : name(n), age(a) {}friend class Display; // 声明友元类
};void Display::show(const Student& s) {cout << "Name: " << s.name << ", Age: " << s.age << endl; // 直接访问私有成员
}// 补充一个友元成员函数
class Student {
private:string name;int age;
public:Student(const string &n, int a) : name(n), age(a) {}friend class Display; // 声明友元类friend void compareStudents(const Student& s1, const Student& s2); // 声明友元成员函数
};// 定义友元成员函数,比较两个Student对象的年龄
void compareStudents(const Student& s1, const Student& s2) {if (s1.age > s2.age) {cout << s1.name << " is older than " << s2.name << endl;} else if (s1.age < s2.age) {cout << s2.name << " is older than " << s1.name << endl;} else {cout << s1.name << " and " << s2.name << " are the same age" << endl;}
}int main() {Student s1("Alice", 20);Student s2("Bob", 22);// 使用友元函数printStudent(s1); // 输出: Name: Alice, Age: 20// 使用友元类Display d;d.show(s2); // 输出: Name: Bob, Age: 22// 使用友元成员函数compareStudents(s1, s2); // 输出: Bob is older than Alicereturn 0;
}
运算符重载(Operator Overloading)
一、运算符重载(Operator Overloading)
1. 核心概念
-
定义:赋予运算符对自定义类类型对象的功能。
-
格式:
返回类型 operator运算符(参数列表)
。 -
形式:
-
非成员函数:通常声明为友元以访问私有成员。
-
成员函数:隐含
this
指针,左操作数为当前对象。
-
2. 常见运算符重载规则
-
加法运算符(+)成员函数:
String String::operator+(const String &op2){int len = strlen(this->str)+strlen(op2.str)+1;String newStr;newStr.str = new char[len];strcpy(newStr.str,this->str);strcat(newStr.str,op2.str);return newStr; }
-
自增运算符(++):
-
前置++:
String& operator++();
-
后置++:
String operator++(int);
(通过哑元参数区分)
-
-
下标运算符([]):
char &String::operator[](int index)const{//检查索引是否越界,必须满足在固定的长度内if (index < 0 || index >= this->size) {static char nullChar = '\0'; // 定义一个静态字符return nullChar; // 返回静态字符的引用}return this->str[index]; }
3. 关键注意事项
-
内存管理:运算符重载中若涉及动态内存(如字符串拼接),需实现深拷贝。
-
返回值优化:返回临时对象时,优先通过构造函数优化(如
return String(buffer);
)。 -
异常处理:下标访问需检查索引范围,防止越界。
4. 示例代码
// 加法运算符重载(非成员函数)
String operator+(const String& a, const String& b) {char* buffer = new char[a.length + b.length + 1];strcpy(buffer, a.str);strcat(buffer, b.str);String result(buffer);delete[] buffer;return result;
}// 自增运算符重载(成员函数)
String& String::operator++() { // 前置++for (char* p = str; *p; p++) (*p)++;return *this;
}String String::operator++(int) { // 后置++String temp(*this); // 调用拷贝构造函数++(*this); // 调用前置++return temp;
}
2. 实现String类的下标运算符重载
char &String::operator[](int index)const{//检查索引是否越界,必须满足在固定的长度内if (index < 0 || index >= this->size) {static char nullChar = '\0'; // 定义一个静态字符return nullChar; // 返回静态字符的引用}return this->str[index];
}
二、赋值运算符(=)重载
-
核心作用
实现对象间的深拷贝,避免浅拷贝导致的内存重复释放或泄漏。 -
实现要点
-
自赋值检查:防止
a = a
导致内存错误。 -
释放旧内存:赋值前需释放当前对象的资源。
-
深拷贝新内存:重新分配内存并复制内容。
-
-
代码示例
String &String::operator=(const String &obj) {if (this != &obj){ // Exclude the scenario where this points to objif (this->str != NULL){delete[] this->str;}if (obj.str != NULL){int len = strlen(obj.str) + 1;this->str = new char[len];strcpy(this->str, obj.str);}}return *this; }
-
注意事项
-
必须为成员函数:C++规定赋值运算符只能通过成员函数重载。
-
默认赋值运算符的陷阱:默认实现为浅拷贝,需手动重载以支持深拷贝。
-
三、左移运算符(<<)重载
-
核心作用
支持自定义类的输出流操作(如cout << obj
)。 -
实现要点
-
声明为友元函数:以访问类的私有成员。
-
返回
ostream&
:支持链式调用(如cout << a << b
)。
-
-
代码示例
两种实现方法 1、 ostream &operator<<(ostream &os, const String &obj) {if (obj.str){os << obj.str;}return os; } 2、 class String { private:char* str; public:friend ostream& operator<<(ostream& os, const String& obj); };ostream& operator<<(ostream& os, const String& obj) {if (obj.str != nullptr) {os << obj.str;}return os; // 返回流引用以支持链式调用 }
-
注意事项
-
输入运算符(>>)同理:需处理输入流并修改对象状态,通常声明为友元。
-
四、不能被重载的运算符
运算符 | 不可重载原因 |
---|---|
. | 成员访问运算符,重载会破坏语言基础结构。 |
:: | 域运算符,属于编译时解析的语法结构。 |
sizeof | 计算对象大小,编译时确定,无法动态重载。 |
?: | 三目运算符,逻辑复杂且可能引发歧义。 |
.* 和 ->* | 成员指针访问运算符,语法特殊且用途有限。 |
五、运算符重载的通用规则
-
语法限制
-
只能重载C++已有的运算符,不可创建新运算符(如
**
)。 -
不能改变运算符的优先级、结合性或操作数个数(如
+
始终为双目运算符)。
-
-
函数形式
-
成员函数:适用于左操作数为当前类对象(如
a + b
)。 -
友元函数:适用于左操作数为其他类型(如
cout << obj
)。
-
-
特殊运算符重载
-
自增/自减运算符:
String& operator++(); // 前置++ String operator++(int); // 后置++(通过哑元参数区分)
-
下标运算符([]):需提供
const
和非const
版本。char &String::operator[](int index)const{//检查索引是否越界,必须满足在固定的长度内if (index < 0 || index >= this->size) {static char nullChar = '\0'; // 定义一个静态字符return nullChar; // 返回静态字符的引用}return this->str[index]; }
-
六、常见问题与解决方案
问题 | 原因 | 解决方法 |
---|---|---|
内存泄漏 | 未释放旧内存 | 在赋值运算符中先 delete[] 再 new |
双重释放(double free) | 浅拷贝导致共享内存 | 实现深拷贝 |
链式调用失败 | 未返回流或对象引用 | 返回 ostream& 或 String& |
继承
一、继承的核心概念
-
作用
-
减少代码冗余:将多个类的公共属性和方法抽象到父类中,子类通过继承复用代码。
-
增强扩展性:子类可添加特有属性或覆盖父类方法,实现功能扩展。
-
-
基本语法
class 子类名 : 访问修饰符 父类名 {// 子类特有属性和方法 };
-
访问修饰符:
public
、protected
、private
,决定父类成员在子类中的可见性。
-
二、用户代码分析
-
原始问题
-
多个英雄类(如
Hanzin
、Houyi
)中存在重复的“英雄的属性”,导致代码冗余。
-
-
优化思路
-
将公共属性提取到基类
Hero
中,特定类型(如射手、法师)进一步抽象为中间类(如Ranger
),最终由具体英雄类继承。
-
三、继承体系设计示例
-
基类:
Hero
#ifndef __HERO_HEAD__ #define __HERO_HEAD__ #include <iostream> #include <string> using namespace std; class Hero { public:// 重构函数Hero(const string &name = "hero") : name(name) {};// 析构函数~Hero(){cout << "Hero 析构" << endl;} //英雄特有的名字给一个函数用于访问namestring get_name() const { return name; }void set_name(const string &name) { this->name = name; }virtual void show() const{cout << "Name: " << name << endl;} protected:string name; };#endif
-
中间类:Ranger(射手)
#ifndef __RANGER_HEAD__ #define __RANGER_HEAD__ #include "hero.h" #include <iostream> using namespace std; class Ranger{ public: //构造函数Ranger(int distance):distance(distance){cout << "Ranger 构造函数" << endl;} //获取对应的射程int get_distance() const { return distance; }void set_distance(int distance) { this->distance = distance; }void show(){cout << "Distance: " << distance << endl;}~Ranger() {cout << "Ranger 析构函数" << endl;}private:int distance; };#endif
-
具体类:Drj(狄仁杰)
#ifndef DRJ_H #define DRJ_H #include "hero.h" #include "ss.h" #include <string> #include <iostream>class Drj : public Ranger,public Hero{ // 仅继承 Ranger(已虚继承 Hero) private:std::string looks;public:Drj(const std::string &name, int distance, const std::string &looks):Hero(name),Ranger(distance),looks(looks){std::cout << "Drj 构造函数" << std::endl;}void show()const{Hero::show(); // 调用 Hero 的 show()std::cout << "Looks: " << looks << std::endl;}~Drj(){std::cout << "Drj 析构函数" << std::endl;} };#endif
4.Hero_main.cpp:
#include <iostream>
#include "hero.h"
#include "ss.h"
#include "drj.h"int main() {/*若是直接用private:或protected:继承都会出现继承范围越来越小,以至于在主函数中都无法使用因此利用public直接继承值或是public函数辅助访问私有或是保护的值*/// 创建 Hero 对象Hero hero("基础英雄");hero.show();std::cout << "-----------------" << std::endl;// 创建 Ranger 对象攻击范围是100Ranger ranger(100);ranger.show();std::cout << "-----------------" << std::endl;// 创建 Drj 对象Drj drj("狄仁杰", 150, "英俊");drj.show();std::cout << "-----------------" << std::endl;return 0;
}
结果显示:
六、继承的优势与注意事项
-
优势
-
代码复用:公共逻辑集中管理,减少重复代码。
-
层次化设计:通过多级继承实现模块化开发(如
Hero → Ranger → Houyi
)。
-
-
注意事项
-
访问权限:合理使用
public
、protected
、private
控制成员可见性。 -
菱形继承问题:避免多继承导致的二义性(可通过虚继承解决)。
-
父类析构函数:若父类有虚函数,应声明虚析构函数以防止资源泄漏。
-
关键实践建议:
-
优先使用组合而非继承,避免过度设计。
-
使用
protected
替代private
以支持子类扩展。 -
在复杂继承体系中,合理使用虚函数和多态特性。
四、菱形继承
1. 菱形继承的定义与结构
菱形继承(Diamond Inheritance) 是指一个派生类(如 D
)通过多个路径继承自同一个基类(如 A
)。具体结构如下:
-
示例代码结构:
class A { public: int a; }; class B : public A {}; class C : public A {}; class D : public B, public C {};
2. 菱形继承导致的问题
-
成员重复:
由于B
和C
都继承自A
,D
会包含两份A
的成员变量a
(分别通过B
和C
继承)。 -
访问歧义:
当在D
中直接访问a
时,编译器无法确定应使用B::a
还是C::a
,导致编译错误。
示例错误代码:
class D : public B, public C {
public: void function() { a = 200; // ❌ 错误:reference to 'a' is ambiguous }
}; int main() { D object; object.a = 100; // ❌ 错误:request for member 'a' is ambiguous return 0;
}
3. 编译器报错原因
-
歧义性访问:
D
中存在两个独立的A
子对象(分别来自B
和C
),因此a
在D
中有两个副本。 -
错误示例:
Test.cpp:18:17: error: reference to 'a' is ambiguous Test.cpp:25:16: error: request for member 'a' is ambiguous
4. 解决方法
通过 作用域解析运算符(::
) 明确指定访问路径,消除歧义。
修改后代码:
class D : public B, public C {
public: void function() { B::a = 200; // ✅ 明确指定访问 B 继承的 a }
}; int main() { D object; object.C::a = 100; // ✅ 明确指定访问 C 继承的 a return 0;
}
5. 其他解决方法(补充)
-
虚继承(Virtual Inheritance):
使用virtual
关键字继承,确保D
中只保留一份A
的成员。class B : virtual public A {}; class C : virtual public A {}; class D : public B, public C {};
-
优势:无需通过作用域解析符访问,直接使用
a
。 -
注意:虚继承会增加对象内存布局的复杂性。
-
6. 关键结论
问题 | 原因 | 解决方案 |
---|---|---|
成员重复与访问歧义 | 多路径继承同一基类 | 使用 B::a 或 C::a 明确路径 |
代码冗余 | 多个基类副本 | 虚继承(推荐) |
五、访问修饰符
1、类成员的访问修饰符
修饰符 | 类内部访问 | 子类访问 | 类外访问 |
---|---|---|---|
public | ✅ | ✅ | ✅ |
protected | ✅ | ✅ | ❌ |
private | ✅ | ❌ | ❌ |
核心规则:
-
public
:完全开放访问。 -
protected
:仅限类内部和子类访问。 -
private
:仅限类内部访问。
2、继承方式对成员权限的影响
继承方式 | 基类 public 成员 | 基类 protected 成员 | 基类 private 成员 |
---|---|---|---|
public | 保持 public | 保持 protected | 不可访问 |
protected | 降级为 protected | 保持 protected | 不可访问 |
private | 降级为 private | 降级为 private | 不可访问 |
核心规则:
-
继承方式决定了基类成员在派生类中的访问权限上限。
-
private
成员始终不可被派生类直接访问。
3、示例代码分析
基类定义
-
B1
类:class B1 { public:int i; // public protected:int k; // protected };
-
B2
类(修正变量名1
为l
):class B2 { public:int l; // public private:int m; // private protected:int q; // protected };
-
B3
类:class B3 { public:int p1; // public };
派生类 C
的继承方式
class C : public B2, protected B1, private B3 {
public:int c; // 新增 public 成员
};
成员权限推导
基类成员 | 原始权限 | 继承方式 | 在 C 中的权限 | 是否可外部访问 |
---|---|---|---|---|
B2::l | public | public 继承 | public | ✅ |
B2::q | protected | public 继承 | protected | ❌ |
B1::i | public | protected 继承 | protected | ❌ |
B1::k | protected | protected 继承 | protected | ❌ |
B3::p1 | public | private 继承 | private | ❌ |
C::c | - | - | public | ✅ |
组合
一、组合的核心概念
-
定义:组合是通过将现有类的对象作为成员嵌入到新类中,构建“整体-部分”关系(即“has-a”或“contains-a”关系)。
-
典型场景:
-
汽车(
Car
)包含引擎(Engine
)和轮胎(Wheel
)。 -
英雄(
Houyi
)拥有皮肤(Skin
)。
-
二、组合的实现方式
1. 基本语法
class NewClass {
public:// 成员函数
private:Class1 obj1; // 组合:包含 Class1 对象Class2 obj2; // 组合:包含 Class2 对象
};
2. 示例代码分析
class Houyi : public Hero, public Ranger {
public:Houyi(const string &name, int distance, int looks, int price, const string &appearance);~Houyi();void show() const;private:int looks;Skin skin; // 组合:Skin 对象作为成员
};// 构造函数初始化列表必须显式初始化成员对象
Houyi::Houyi(...) : Hero(name), Ranger(distance), skin(price, appearance) {// 派生类自身逻辑
}
3. 关键规则
-
成员对象初始化:必须在构造函数的初始化列表中显式调用成员对象的构造函数。
-
默认构造函数:若未显式初始化,成员对象会调用其默认构造函数(若存在)。
三、构造函数与析构函数的调用顺序
-
构造函数调用顺序:
-
按继承顺序调用基类构造函数(
Hero
→Ranger
)。 -
按成员对象声明顺序调用成员对象的构造函数(
Skin
)。 -
最后调用派生类自身的构造函数(
Houyi
)。
-
-
析构函数调用顺序:与构造函数顺序相反。
-
先调用派生类的析构函数。
-
再按成员对象声明逆序调用析构函数(
Skin
)。 -
最后按继承逆序调用基类析构函数(
Ranger
→Hero
)。
-
四、继承与组合的对比
特性 | 继承(is-a) | 组合(has-a) |
---|---|---|
关系类型 | 子类是父类的一种特化 | 整体包含部分 |
耦合度 | 高耦合(父类改动影响子类) | 低耦合(通过接口交互) |
代码复用 | 直接复用父类方法 | 通过成员对象复用功能 |
灵活性 | 低(编译时绑定) | 高(运行时动态替换成员对象) |
设计原则 | 易违反封装原则 | 符合信息隐藏原则 |
五、组合的优缺点
优点
-
低耦合:成员对象通过接口交互,隐藏实现细节。
-
动态扩展:可在运行时替换成员对象(例如更换引擎)。
-
强封装性:成员对象的内部细节对整体类不可见。
缺点
-
对象数量增多:可能导致内存占用增加。
-
接口设计复杂:需设计清晰的接口协调多个成员对象。
六、组合的应用示例
任务:为“狄仁杰”类添加 Skin
成员并显示信息
class DiRenJie : public Hero {
public:DiRenJie(const string &name, int skillLevel, const Skin &skin) : Hero(name), skin(skin), skillLevel(skillLevel) {}void showInfo() const {cout << "Name: " << getName() << endl;cout << "Skill Level: " << skillLevel << endl;cout << "Skin: " << skin.getAppearance() << " (Price: " << skin.getPrice() << ")" << endl;}private:int skillLevel;Skin skin; // 组合:包含 Skin 对象
};
- 这是本人的学习笔记不是获利的工具,小作者会一直写下去,希望大家能多多监督我
- 文章会每攒够两篇进行更新发布(受平台原因,也是希望能让更多的人看见)
- 感谢各位的阅读希望我的文章会对诸君有所帮助