运算符重载之章
加法运算符重载39
1,成员函数重载
2,全局函数重载
运算符重载主要用于自定义类型运算,也可以使用函数重载。
class Person{
public:
int m_a;
int m_b;
Person operator+ (Person &p){
Person temp;
temp.m_a = this -> m_a + p.m_a;
temp.m_b = this -> m_b + p.m_b;
return temp;
}
Person operator+ (Person &p ,int num){
Person temp;
temp.m_a = this -> m_a + num;
temp.m_b = this -> m_b + num;
return temp;
}
}
Person operator+ (Person &p1, Person &p2){
Person temp;
temp.m_a = p1 -> m_a + p2.m_a;
temp.m_b = p2 -> m_b + p1.m_b;
return temp;
}
void test01(){
Person p1;
p1.m_a = 10;
p1.m_b = 10;
Person p2;
p2.m_a = 10;
p2.m_b = 10;
Person p3 = p1 + p2;
Person p4 = p1+100;
}
左移运算符重载40
不用成员函数做左移运算符重载的原因是格式不好看(无法实现cout在左侧),用去全局函数做重载,传入ostream对象cout,和类对象,再应用一些链式编程思想,即可完成连续输出。
class Person{
public:
int age;
string name;
//p.operator<<(p)
(x)Person operator<< (Person &p);
//p << cout
(x)Person operator<< (ostream &cout);
}
//standard out stream object only one in whole situation
ostream & operator<< (ostream & cout , Person & p){
cout<<"age : "<<p->age<<" Name : "<< o->name <<endl;
return cout;
}
自增/自减运算符重载41
要是没有黑马的教程我根本就注意不到这二者的区别:
int a = 10;
--(--a);
(x)(a--)--;
表达式 | 返回类型 | 是否合法 | 原因 |
---|---|---|---|
--(--a) | 左值 | 合法 | 可以对左值连续递减 |
(a--)-- | 右值 | 非法 | 不能对右值递减 |
- 核心区别:前置递减返回左值,后置递减返回右值。
- 设计逻辑:后置递减需要返回原始值(临时值),因此不能支持连续递减。
class Czpp {
friend ostream& operator<< (ostream& cout, Czpp c01);
public:
Czpp() {
o = 0;
}
//the reason of add & becaues ++op is design for continuous output.
//So we can call ++(++a) trustingly
Czpp& operator++ () {
o++;
return *this;
}
Czpp operator++ (int) {
Czpp temp = *this;
o++;
return temp;
}
private:
int o;
};
ostream& operator<< (ostream& cout, Czpp c01) {
cout << c01.o << endl;
return cout;
}
void testcase(){
Czpp c01;
cout << ++(++c01) << endl;
cout << c01 << endl;
cout << ((c01++)++)++ << endl;
cout << c01 << endl;
}
class Czmm {
friend ostream& operator<< (ostream& cout, Czmm c01);
public:
Czmm() {
oi = 100;
}
Czmm(int n) {
oi = n;
}
Czmm & operator-- () {
oi
--;
return *this;
}
Czmm operator--(int) {
Czmm temp = *this;
oi--;
return temp;
}
private:
int oi;
};
ostream& operator<< (ostream& cout, Czmm c01) {
cout << c01.oi << endl;
return cout;
}
void test02() {
Czmm c02;
cout << --c02 << endl;
cout << --(--c02) << endl;
Czmm c03(20);
cout << c03-- << endl;
cout << c03 << endl;
}
赋值运算符重载42
C++编译器会给一个类添加四个函数:
1,默认构造函数,函数体为空。
2,默认析构函数,函数体为空。
3,默认拷贝构造函数,对属性进行值拷贝。
4,赋值运算符 operator= 对属性进行值拷贝。
我们在类外进行类的等号赋值操作时,进行的是浅拷贝,容易触发地址重复释放问题。可以通过重载赋值运算符进行深拷贝规避问题。
class Person{
public:
int* m_age;
Person(int age){
m_age = new int(age);
}
//add operator= heavyload
//chain programming ideas
Person & operator= (Person& p){
//Compiler offer shallow copy
//m_age = p.m_age;
//Defensive programming
//Chenking if this property is in heap area
if(m_age!=NULL){
delete m_age;
m_age = NULL;
}
m_age = new int (*p.m_age);
return *this;
}
~Person(){
if(m_age != NULL){
delete m_age;
m_age = NULL;
}
}
};
void test01(){
Person p1(18);
Person p2(20);
//assignment operatation will trigger shallow copy
//Repetition release heap area memory
p1 = p2;
cout<<*p1.m_age<<endl;
cout<<*p2.m_age<<endl;
}
关系运算符重载43
涉及>,<,==,!=等,返回bool类型值。
函数调用重载运算符&匿名函数对象44
函数调用()也可以重载
class Myprint{
public:
void operator()(string rest){
cout<<rest<<endl;
}
};
void test01(){
Myprint mp;
mp("hello world");
//anonymous func object
Myprint()("hello world");
}
继承之章
初识继承45
公共部分抽象为父类,子类继承
继承方式46
继承中的对象模型47
子类继承父类,会得到所有父类成员变量,只是会自动隐藏父类的私有成员变量。通过vs Developer Command Prompt查看(报告单个类的布局)
cl /d1 reportSingleClassLayout类名 “cpp文件名”
继承中的构造以及析构顺序48
父类先构造,后析构
同名成员处理49
如果子类中出现和父类同名的成员函数,子类的同名成员函数会自动隐藏掉父类中所有的同名成员函数,在有子类的情况下,(即使有能明显区分的重载也不行)访问父类中被隐藏的同名成员函数,需要加作用域。
同名静态成员处理50
1,通过对象访问
2,通过类名访问
class father {
public:
int fapuint;
static int fapusint;
protected:
int faprint;
static int faprsint;
private:
int fapriint;
static int faprisint;
public:
father() {
fapuint = 10;
//I forget how to init static ^-^
/*fapusint = 100;*/
faprint = 20;
fapriint = 30;
}
void pint() {
cout << "father func" << endl;
}
static void spint() {
cout << "father static func" << endl;
}
};
int father::fapusint = 100;
class son : public father {
public:
static int fapusint;
void pint() {
cout << "son func" << endl;
}
static void spint() {
cout << "son static func" << endl;
}
};
int son::fapusint = 100;
void tcinher() {
son s1;
//access from obj
cout << "son ' s varible access with obj" << s1.fapuint << endl;
cout << "fa ' s varible access with obj" << s1.father::fapuint << endl;
//access from action scope(class name)
//This access way is only ok to static varible / func.
cout << "son ' s static varible access with class name" << son::fapusint << endl;
cout << "fa ' s static varible access with class name" << son::father::fapusint << endl;
//son ' s func access with obj
s1.pint();
//fa ' s func access with obj
s1.father::pint();
//son ' s static func access with class name
son::spint();
//fa ' s static func access with class name
son::father:: spint();
}
继承语法51
多继承,注意以作用域区分同名。
class son : public base1, public base2{
};
菱形继承52
虚基类,虚继承
情况 | 继承方式 | grandfa 子对象数量 | 访问 m_age 是否二义 |
---|---|---|---|
普通继承 | class fath : public grandfa | 2 份(fath + math ) | 是(需指定路径) |
虚继承 | class fath : virtual public grandfa | 1 份(共享) | 否(可直接访问) |
fath
和math
共享同一个grandfa
子对象。sons
只包含 一个grandfa
实例,避免了二义性。
底层实现(编译器如何支持虚继承?)
虚继承的实现通常依赖 虚基类表(Virtual Base Table, vbtable) :
-
虚基类指针(vbptr) :
- 当某个类虚继承自另一个类时,编译器会为该类添加一个 虚基类指针(vbptr) ,指向虚基类表(vbtable)。
-
虚基类表(vbtable) :
- 存储了虚基类相对于当前对象的偏移量。
- 这样,即使
fath
和math
在sons
中的位置不同,它们也能正确找到共享的grandfa
子对象。
class grandfa {
public:
int m_age;
};
//class uncle : virtual public grandfa
class uncle : public grandfa {
public:
};
//class math : virtual public grandfa
class math : public grandfa {
public:
};
//class fath : virtual public grandfa
class fath : public grandfa {
public:
};
class sons :public fath, public math , public uncle {
public:
};
void testv() {
sons son1;
son1.uncle::m_age = 77;
son1.math::m_age = 19;
/*son1.fath::m_age = 20;*/
son1.grandfa::m_age = 100;
//cout << "son's age = " <<son1.m_age << endl;
cout << "uncle's age = " << son1.uncle::m_age << endl; // 77(未被修改)
/*cout << "fath's age = " << son1.fath::m_age << endl;*/
cout << "math's age = " << son1.math::m_age << endl; // 19(未被修改)
cout << "gf's age = " << son1.grandfa::m_age << endl; // 100(修改的是 fath 的版本)
}
实际上我在看到多态原理剖析时,用sizeof测试了各个类的大小:
cout << "size of gf " << sizeof(grandfa) << endl;//4
cout << "size of uncle " << sizeof(uncle) << endl;//16
cout << "size of math " << sizeof(math) << endl;//16
cout << "size of fath " << sizeof(fath) << endl;//16
cout << "size of sons " << sizeof(sons) << endl;//32
Deepseek声称可能存在内存对齐,虚基类指针占用8位,对齐4位,继承的int4位。
在不使用虚继承时出现的情况:
-
son1.grandfa::m_age = 100;
是 模糊访问,但某些编译器(如 MSVC)会 隐式选择第一个基类(fath
)的grandfa
,因此:- 它修改的是
fath
的grandfa::m_age
(但fath::m_age
之前未被赋值,可能是随机值或 0)。 uncle::m_age
和math::m_age
未被影响,仍然是77
和19
。
- 它修改的是
-
输出时:
son1.uncle::m_age
是77
(未被grandfa::m_age = 100
影响)。son1.math::m_age
是19
(未被影响)。son1.grandfa::m_age
是100
(修改的是fath
的版本)。
问题:为什么 grandfa::m_age = 100
不影响 uncle::m_age
和 math::m_age
?
因为:
sons
有 3 份独立的grandfa
子对象(fath
、math
、uncle
各一份)。son1.grandfa::m_age
只能修改其中一份(具体是哪份取决于编译器,这里是fath
的)。uncle::m_age
和math::m_age
是另外两份,不受影响。
EX:继承中的父子类思考:
为什么子类构造时要求父类必须有默认构造函数?
在 C++ 中,子类对象的构造过程一定会先构造父类部分。如果父类没有默认构造函数(无参构造函数),而子类的构造函数又没有显式调用父类的某个构造函数,编译器就无法自动构造父类,从而导致编译错误。
1. 构造顺序:父类 → 子类
当创建一个子类对象时,构造顺序是:
- 父类的构造函数(如果父类还有父类,则继续向上递归)
- 子类成员变量的构造函数(按声明顺序)
- 子类自己的构造函数
如果父类没有默认构造函数,而子类又没有显式指定调用父类的哪个构造函数,编译器就不知道如何构造父类部分,因此会报错。
2. 示例分析
❌ 错误情况:父类没有默认构造函数,子类未显式调用父类构造函数
class Parent {
public:
Parent(int x) { } // 只有带参数的构造函数,没有默认构造函数
};
class Child : public Parent {
public:
Child() { } // 错误!编译器不知道如何构造 Parent
};
int main() {
Child c; // 编译失败
return 0;
}
报错信息:
error: no matching function for call to 'Parent::Parent()'
原因: Child()
构造函数没有显式指明如何构造 Parent
,编译器尝试调用 Parent::Parent()
(默认构造函数),但 Parent
没有默认构造函数,因此报错。
✅ 正确做法:
(1)父类提供默认构造函数
class Parent {
public:
Parent() { } // 提供默认构造函数
Parent(int x) { } // 也可以有其他构造函数
};
class Child : public Parent {
public:
Child() { } // 现在可以编译,自动调用 Parent()
};
(2)子类显式调用父类的某个构造函数
class Parent {
public:
Parent(int x) { } // 只有带参数的构造函数
};
class Child : public Parent {
public:
Child() : Parent(42) { } // 显式调用 Parent(int)
};
int main() {
Child c; // 正确,Parent 被构造为 Parent(42)
return 0;
}
关键点:
Child() : Parent(42)
是 成员初始化列表(Member Initializer List) ,在这里显式调用了Parent(int)
。- 如果不写
: Parent(42)
,编译器会尝试调用Parent()
,但Parent
没有默认构造函数,导致编译错误。
3. 更深层次的情况:多重继承
如果子类继承多个父类,且某些父类没有默认构造函数,必须在子类构造函数中显式调用它们的构造函数:
class Parent1 {
public:
Parent1(int x) { }
};
class Parent2 {
public:
Parent2(int y) { }
};
class Child : public Parent1, public Parent2 {
public:
Child() : Parent1(10), Parent2(20) { } // 必须显式调用
};
如果不写 : Parent1(10), Parent2(20)
,编译器会报错!
4. 为什么 C++ 这样设计?
- 明确性:强制程序员显式指定如何构造父类,避免意外行为。
- 灵活性:允许父类的构造依赖子类传递的参数。
- 避免二义性:如果父类有多个构造函数,编译器不知道默认用哪个,必须由程序员指定。
总结
情况 | 父类构造函数 | 子类构造函数写法 | 是否合法 |
---|---|---|---|
父类有默认构造函数 | Parent() 存在 | Child() { } | ✅ 合法 |
父类无默认构造函数 | 只有 Parent(int) | Child() { } | ❌ 编译错误 |
父类无默认构造函数 | 只有 Parent(int) | Child() : Parent(42) { } | ✅ 合法 |
关键规则:
- 如果父类没有默认构造函数,子类必须显式调用父类的某个构造函数。
- 如果父类有默认构造函数,子类可以不显式调用(编译器自动调用
Parent()
)。