【C++】2-类和对象
1. 类的引入
C语言中有结构体struct,但结构体中只能定义变量。
但C++对结构体struct做了扩展,结构体中不仅能定义变量,也能定义函数。
- C++中的结构体被称作类。
- C++中类中的函数被称作成员方法,类中的变量被称作成员变量。
struct Stack
{
// C++写法
// 成员方法/成员函数
Stack init(int capacity)
{
// ..
}
Stack push()
{
// ..
}
// 成员变量
int _capacity;
int _top;
};
- C++兼容C语言,C语言中对结构体的用法,C++同样适用。
// 接续上述代码
void StackInit(struct Stack* st, int capacity)
{}
void StackPush(struct Stack* st, int x)
{}
int main()
{
// C++用法 -- 直接用类名称声明/定义对象(变量)
Stack st;
st.init(10);
st.push(1);
int capacity = st._capacity;
// C语言用法
struct Stack st1;
StackInit(&st1, 10);
StackPush(&st1, 1);
return 0;
}
2. 类的定义
class className
{
// 类体,包括成员函数和成员变量
// ..
};
// class也可以被替换为struct
// 但C++中经常使用class定义类
- class是定义类的关键字,className是类的名字
{}
中是类的主体(简称为类体),类体中的变量是类的属性或成员变量,类体中的函数被称作类的函数或成员方法- {}后面的
;
不能省略!
类的定义有两种方式:
-
声明和定义都在类体中
class Person { public: void showInfo() { // .. } private: char* _name; char* sex; int age; };
成员函数若在类体中定于,那么编译器默认将其视为内联函数处理。
-
声明和定义分离
// Person.h class Person { public: void showInfo(); private: char* _name; char* sex; int age; }; // Person.cpp #include "Person.h" void Person::showInfo() { }
若声明和定义分离,那么在定义时,需在函数名前指定类域,以防出现多个重名函数的定义冲突问题。
3. 类的使用
类是一种高级数据类型,可以直接用数据类型+数据名的方式定义数据,数据可以是变量或者函数(方法)。
语法:[类的名称] [数据名称]
(类的引用中已有示例说明)
4. 类的访问限定符
C++定义了3中访问访问方式或访问权限:
- public :公有
- protected :保护
- private :私有
访问限定符相关说明:
- 限定的只是在类外的访问,即只对在类外对类中数据的访问做限制,在类里面的访问不做限制
- public修饰的数据,可以直接被访问,不做限制
- protected和private修饰的数据在类外不可直接被访问
- 访问权限的作用域指的是从该访问权限出现到下一个访问权限出现为止,若之后再没有访问限定符,那么它的作用域就到类体结束为止
- class定义的类的的默认访问权限是private,即若不对class中的成员做访问限制,那么它的成员的访问权限就是private
- struct定义的类的访问权限是public,因为它要兼容C语言(C语言中访问权限就是public)
5. 类域
域:{}包围起来的部分就是一个域
类域:类定义中类名后面的{}包围的部分,即类体,就是类域。
我们知道命名空间,对于命名空间域,可以通过域作用访问限定符
::
来访问命名空间域中的数据,且命名空间和类的定义类似,那么能否也使用域作用访问限定符**::
**来访问类中的数据呢?namespace A { int a = 10; } class AA { int aa = 10; }; int main() { int b = A::a; cout << b << endl; int bb = AA::aa;// 报错 return 0; }
答案是:不能。
因为,类可以理解为是一个模板或建造设计图,并不是一个实体,类中的成员只是声明。根据这个模板或设计图建造出的实际建筑才是实体。只有有了类的实体,类中的成员才有定义。不能访问只有声明但还没有定义的数据。
类域的作用:
声明和定义分离,增加代码可读性。
对于上述图片中的代码,只有一个Stack类,也只有单一的init和push函数,按照SQ.cpp中的定义不会报错。
但如果有不止一个类,且类中有同名的函数,那么这时若声明和定义分离,还可以按照SQ.cpp中的那样写吗?
很显然,会出现重定义问题。
那么如何解决这种问题呢? – 利用类域
类:声明和定义分离要体现类域
这样就可以了。
6. 类对象的存储方式
计算类对象的大小:
// 例1
class A
{
public:
void func()
{
cout << _a << endl;
}
private:
char _a;
};
int main()
{
A aa;
cout << sizeof(A) << endl;
cout << sizeof(aa) << endl;
return 0;
}
// 输出
1
1
C++兼容C语言,C语言中的结构体内存对齐规则,C++同样适用。
按照结构体内存对齐规则,成员变量_a是char类型,占一个字节,成员函数func在内存中存的是它的地址,地址本质是指针,占4个字节(或8个字节,这里默认是4字节),依照结构体内存对齐规则,类A和它的对象aa应均占8个字节。
但这里为什么输出的却是1呢?这就涉及到了类对象的存储方式。
对象中存放类的所有成员
每个对象中的成员变量可能不同,但成员函数都是相同的,每创建一个对象,成员函数的代码就会拷贝一份,引起空间浪费。
对象中存放类的成员变量和成员函数表的地址
成员函数的代码只保存一份,每个对象中存放成员变量和成员函数表(存放成员函数的空间)的地址,通过这个地址可以访问成员函数。
对象中只存放类的成员变量
成员函数只保存一份,存储在一个公共代码区域,对象中只存放成员变量,若要调用成员函数,只需到这个公共代码区域找即可。
经过比较,第三种对象存储方式效率更高,我们用的也是这种方式。
了解了类对象的存储方式,再看下面的代码:
class B
{
void fun()
{}
};
class C
{};
int main()
{
cout << sizeof(B) << endl;
cout << sizeof(C) << endl;
return 0;
}
// 输出
1
1
不难分析,类B的对象不存储函数B的任何数据(包括函数地址),因此,类B和类C的大小应是一样的。C++默认给这种类的对象一个字节的占位空间,标识同一类的对象不一样。
// 接上述代码
// 定义两个对象
B b;
B bb;
cout << &b << endl;
cout << &bb << endl;
// 输出
1
1
00FDF773
00FDF767
// 这一个占位字节就标识对象b和对象bb不是同一个
7. this指针
先看下面的代码:
class A
{
public:
void Init(int a = 1)
{
_a = a;
}
void Print()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A a1, a2;
a1.Init(10);
a2.Init(20);
a1.Print();
a2.Print();
return 0;
}
// 输出
10
20
对象a1和a2调用的Print函数是一样的(参考类对象存储方式),可为什么它们输出的值确实不一样呢?
C++给每个“非静态成员函数”增加了一个隐藏指针this(指针名只能是关键字this),让该指针指向当前调用该函数的对象,在函数体体中,成员变量的访问实际上都是通过this指针来访问的。注意,this指针不需要用户传递,它是由编译器自己实现的,我们可以在类中成员函数中使用this指针。编译器也会默认使用this指针。
上述部分代码实际上是这样的:
// 类中Print函数的实现实际是这样的:
void Print(A* const this)
{
cout << this->_a << endl;
}
// 主函数中a1调用Print函数实际是这样的:
Print(&a1);
关于this指针和类对象的存储方式,以一道题加深理解:
// code-1
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
//code-2
class B
{
public:
void Print()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
B* p = nullptr;
p->Print();
return 0;
}
// 上述两个代码的最终运行结果是什么?
// A.编译报错 B.运行崩溃 C.正常运行
// 答案是:C B
// 这里很多人会陷入一个误区,那就是p是空指针,对空指针解引用会运行崩溃。
// 但类对象中成员函数是存储在公共代码区(代码段或常量区)的,这里只是传了一个空指针给函数Print中的this指针,调用该函数是直接去代码区找的。
// 与代码1不同的是,代码2在Print函数中对this指针(传的nullptr)即空指针进行了解引用操作,所以会引起程序运行崩溃。
8. 类的6个默认成员函数
特点:
- 类中,用户不写(实现),编译器会自动生成(实现),用户写了,编译器就不生成
- 这些编译器自动生成的成员函数被称作默认成员函数。
8.1 构造函数
功能:定义对象时自动给对象初始化
特性:
-
函数名与类名相同
-
无返回类型,无返回值
-
可重载(一个类可以有多个构造函数)
-
实例化对象时编译器自动调用相应的构造函数来初始化对象
class Date { public: // 无参构造 Date() { _year = 1; _month = 1; _day = 1; } // 有参构造 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; int main() { // 定义的同时初始化 Date d1(2023, 4, 24);// 传参 Date d2; d1.Print(); d2.Print(); return 0; } // 输出 2023-4-24 1-1-1
// 下面实例化对象的方式就不可以 int main() { Date d3();// 这一步编译器不会报错,正常运行 // warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?) d3.Print();// 这一步编译器会报错,编译错误 // error C2228: “.Print”的左边必须有类/结构/联合 return 0; } // 编译器可能判断d3为函数名
// 无参构造 Date() { _year = 1; _month = 1; _day = 1; } // 有参构造 Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } // 有参构造函数若是全缺省,那么调用的时候就会和无参构造函数冲突,即二义性 // 调用报错:error:对重载函数的调用不明确 // 因此,建议只写全缺省的有参构造函数即可
-
若未显式定义构造函数,C++编译器会自动生成一个无参的默认构造函数。
// code-1 class Date { public: void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; d1.Print(); return 0; } // 输出 -858993460/-858993460/-858993460
根据上述代码,不难看出,默认构造函数貌似没起什么作用,这是为什么呢?
-
关于**默认成员函数**:
C++将类型分为内置类型(基本数据类型)和自定义类型。
内置类型就是C++语言本身提供的数据类型,如int、char、double等,以及指针(任意类型,即使是自定义类型);
自定义类型就是用户使用class/struct/union等定义的类型。
C++编译器只对自定义类型成员调用它(自定义类型)的默认构造函数,而对内置类型不做处理。
// code-2 class A { public: A(int a = 10) { _a = a; cout << "A()构造函数" << endl; } private: int _a; }; class Date { public: void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; A _a; }; int main() { Date d1; d1.Print(); return 0; } // 输出 A()构造函数 -858993460/-858993460/-858993460
很明显,在没有显式定义构造函数时,编译器对自定义类型成员_a调用了它的默认构造函数。
那么,如何对上面code-2中的类中成员变量既包含自定义类型又包含内种类型的对象初始化呢?
-
C++11针对内置类型成员不初始化的权限打了补丁:内置类型成员变量在类中声明时可以给默认值(缺省值)
class Date { public: void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year = 1;// 默认值/缺省值 int _month = 1;// 默认值/缺省值 int _day = 1;// 默认值/缺省值 A _a; };
缺省值也可以是malloc的:
class A { public: private: int* _a = (int*)malloc(sizeof(int)*4);// 声明 }; // 这里虽然给成员变量_a给了malloc,但是malloc操作是在C++编译器生成的默认构造函数中调用的,然后赋值给_a
-
关于默认构造函数:无参构造函数、全缺省构造函数、用户不显式定义C++编译器自动生成的构造函数都是默认构造函数。默认构造函数只能有一个!(总结,不传参数就可以直接调用的构造函数就是默认构造函数)
8.2 析构函数
功能:销毁对象时,自动清理对象中的资源(与Destory函数类似)
特性:
-
函数名是在类名前加
~
,即~类名
-
无返回类型、无返回值、无参数(因此不能重载,一个类也就只能有一个析构函数)
-
对象生命周期结束时,C++编译系统自动调用析构函数
-
若未显式定义,即用户未实现,那么C++编译器会自动生成一个默认析构函数
同构造函数一样,C++编译器只对自定义类型成员调用它(自定义类型)的默认构造函数,而对内置类型不做处理。
例:
class Stack
{
public:
// 构造函数
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
perror("fail malloc");
return;
}
_capacity = capacity;
_top = 0;
}
/*
其他函数
*/
// 析构函数
~Stack()
{
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
析构顺序:
若在同一块域中定义了多个同类型的自定义类型对象,那么析构顺序是:后定义的对象先被析构。
8.3 拷贝构造函数
8.3.1 功能
实例化一个对象时,拷贝一份同类型的已经存在的实例对象赋值给这个正在实例化的对象。
8.3.2 特点
-
是构造函数的一个重载
-
参数只有一个,且是该类对象的引用(传值方式会引发无穷递归,报错:error C2652: “Date”: 非法的复制构造函数: 第一个参数不应是“Date” )
class Date { public: // 构造 Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } // 拷贝构造 Date(Date& d) { _year = d._year; _month = d._month; _day = d._day; } void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year = 1; int _month = 1; int _day = 1; }; int main() { Date d1(2023, 4, 24);// 构造 Date d2(d1);// 拷贝构造 d1.Print(); d2.Print(); return 0; }
-
若用户没有显式定义,那么C++编译器会自动生成默认拷贝构造函数。
这种拷贝构造函数拷贝对象是:内置类型按照内存存储字节序拷贝。
这种拷贝方式被称作浅拷贝或值拷贝
class Stack { public: Stack(int capacity = 4) { _a = (int*)malloc(sizeof(int) * capacity); if (_a == nullptr) { perror("fail malloc"); return; } _capacity = capacity; _top = 0; } /**/ ~Stack() { free(_a); _a = nullptr; _capacity = _top = 0; } private: int* _a; int _top; int _capacity; }; int main() { Stack st1; Stack st2(st1); return 0; }
上述代码最终会运行崩溃!
原因是同一份代码析构了两次:
拷贝构造得到对象st2,但是,st2和st1中的成员变量_a的地址是一样的,也就是说它俩指向同一块存储空间。析构时(后定义的先析构,即先析构st2,再析构st1),st1和st2都要调用析构函数,那么_ _a所指向的空间就被free了两次。
这种只把(指针变量)_a本身的字节序拷贝过去,但不拷贝其所指向的空间,这种拷贝方式就是浅拷贝。
-
深拷贝:拷贝指针变量时,不仅拷贝指针变量的值,其所指向的空间也会被拷贝过去。
// 拷贝构造 Stack(const Stack& st) { _a = (int*)malloc(sizeof(int) * st._capacity); if (_a == nullptr) { perror("fail malloc"); return; } memcpy(_a, st._a, sizeof(int) * st._top); _capacity = st._capacity; _top = st._top; } // 拷贝构造这样写就可以了
总结:需要写析构函数的都需要写深拷贝,不需要写析构函数的用C++编译器自动生成的默认析构函数(浅拷贝)就可以。
8.3.3 参数注意
-
参数只有一个,且是引用数据类型
为什么不能传值?
class Date { public: // 构造 Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } // 拷贝构造 Date(Date& d) { cout << "Date()拷贝构造" << endl; _year = d._year; _month = d._month; _day = d._day; } private: int _year = 1; int _month = 1; int _day = 1; }; // 传值 void func1(Date d)// 形参是实参的拷贝,会调用Date的拷贝构造 { cout << "func1()" << endl; } // 传引用 void func2(Date& d)// 形参是实参的别名 { cout << "func2()" << endl; } int main() { Date d1(2023, 4, 24); func1(d1); func2(d1); return 0; } // 输出 Date()拷贝构造 func1() func2() // func1传值传参,调用了拷贝构造 // 而func2传引用传参,没有调用拷贝构造
因此,拷贝构造函数的实现应是传引用传参,而非传值传参。
-
参数应加const修饰
// 拷贝构造 Date(const Date& d) { _year = d._year; _month = d._month; d._month = _month;// 这是为了避免这种情况 }
8.4 赋值运算符重载
8.4.1 运算符重载
8.4.1.1概念
为什么要有运算符重载?——让自定义类型对象也可以使用运算符
运算符重载是具有特殊函数名的函数。
函数原型:返回值类型 operator操作符(参数列表)
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
public:
int _year;
int _month;
int _day;
};
// 运算符“==”重载
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main()
{
Date d1(2023, 4, 26);
Date d2(2023, 4, 26);
cout << (d1 == d2) << endl;// 编译器实际上是转换成显式调用
cout << (operator==(d1, d2)) << endl;// 显示调用,但不常用
return 0;
}
// 输出
1
1
上述代码有些不妥,因为类的成员变量的访问权限一般是私有的,但这里为了方便展示示例,暂时设置成了公有访问权限。
解决方法:
// 方法一:运算符重载函数放到类里面
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
private:
int _year;
int _month;
int _day;
};
// 这时的显式调用应该是这样
d1.operator==(d2)
// 方法二:友元声明 - 暂不详述
class Date
{
friend operator==(const Date& d);// 友元声明可以放到类中的任意位置
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
8.4.1.2 运算符重载要求
- 运算符须是已经存在的运算符(只是实现了类的相关运算),不能凭空创造!
- 运算符重载函数必须至少有一个类类型的参数(即自定义类型参数)。
- 运算符重载不能改变内置类型的运算规则,即参数不能全是内置类型(实际与上一条一样,只是换个说法)
- 这5个运算符不能重载:
.* :: ?: . sizeof
8.4.2 赋值运算符重载
与拷贝构造不同的是,赋值重载是将一个已经存在的对象复制拷贝给另一个已经存在的对象。
// 赋值运算符重载 -begin
Date& operator=(const Date& d)// 返回类型应是Date,以适应链式赋值,例d1 = d2 = d3
{
if (this != &d)// 防止自己给自己赋值,深拷贝体现重要作用
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;// 有返回类型Date,所以需要有返回值
// return d;// 不能这样写,const Date -> Date 权限放大
}
// -end
int main()
{
Date d1;
Date d2(2023, 4, 26);
Date d3(d2);// 拷贝构造
d1 = d2;// 赋值重载(复制拷贝)
Date d4 = d2;// 注意,这个是 拷贝构造 -- 实例化对象时初始化
d2.Print();
d3.Print();
return 0;
}
// 输出
2023/4/26
2023/4/26
特点:
-
用户未显示定义,C++编译器会自动生成一个默认复制重载函数。
-
这个C++编译器自动生成的默认复制重载函数,对内置类型做值拷贝,对自定义类型则调用它(自定义类型)自己的默认复制重载函数。
同拷贝构造一样,不需要要显式定义析构的,就不用显式定义赋值重载。
8.5 取地址重载
也是运算符重载,但很少显式定义。
// 1.
Date* operator&()
{
return this;
}
// 2.const
const Date* operator&() const
{
return this;
}
9. 日期类
- Date.h
#pragma once
#include <iostream>
#include <cmath>
using namespace std;
class Date
{
public:
// 友元声明 使得类外的函数也可以调用类中的私有属性
friend ostream& operator<< (ostream& out, const Date& d);
friend istream& operator>> (istream& in, const Date& d);
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1);
// 拷贝构造函数
// d2(d1)
Date(const Date& d);
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d);
// 析构函数
~Date();
// 日期+=天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day) const;
// 日期-天数
Date operator-(int day) const;
// 日期-=天数
Date& operator-=(int day);
// 前置++
Date& operator++();
// 后置++
Date operator++(int);
// 后置--
Date operator--(int);
// 前置--
Date& operator--();
// >运算符重载
bool operator>(const Date& d) const;
// ==运算符重载
bool operator==(const Date& d) const;
// >=运算符重载
bool operator >= (const Date& d) const;
// <运算符重载
bool operator < (const Date& d) const;
// <=运算符重载
bool operator <= (const Date& d) const;
// !=运算符重载
bool operator != (const Date& d) const;
// 日期-日期 返回天数
int operator-(const Date& d) const;
private:
int _year;// 年
int _month;// 月
int _day;// 日
};
// 输入(流插入)输出(流提取)运算符重载
// 返回类型设置为ostream是为了迎合cout << d1 << d2;这种情况的发生,>>同样如此
// 添加内联属性是因为这个功能代码量小,且可能频繁调用
// 如果不加内联则需要将定义放置在Date.cpp中,而非Date.h中,否则会出现链接错误:fatal error LNK1169: 找到一个或多个多重定义的符号
// 此外,若不加内联处理,会报警告:如递归所有控件路径,函数将导致运行时堆栈溢出。
// 此警告明显说明不见内联效率低下
inline ostream& operator<< (ostream& out, const Date& d)
{
out << d._year << " " << d._month << " " << d._day << endl;
return out;
}
inline istream& operator>> (istream& in, const Date& d)
{
in >> d._year >> d._month >> d._day;
}
- Date.cpp
#include "Date.h"
// 判断闰年
static bool IsLeapYear(int year)
{
return (year % 400 == 0) || ((year % 4 == 0) && (year % 100 != 0));
}
// 获取某年某月的天数
static int GetMonthDay(int year, int month)
{
int MonthDay[] = { 0, 31,28,31,30,31,30,31,31,30,31,30,31 };
if (IsLeapYear(year) && month == 2)
return 29;
return MonthDay[month];
}
// 全缺省的构造函数
Date::Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{
if (!(year >= 0 && (month >= 1 && month <= 12) && (day >= 0 && day <= GetMonthDay(year, month))))
{
cout << "日期非法" << endl;
}
}
// 拷贝构造函数
// d2(d1)
Date::Date(const Date& d)
: _year(d._year)
, _month(d._month)
, _day(d._day)
{}
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& Date::operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
// 析构函数
Date::~Date()
{
_year = 0;
_month = 0;
_day = 0;
}
// 日期+=天数
Date& Date::operator+=(int day)
{
if (day < 0)
return *this -= abs(day);
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13)
{
_month = 1;
++_year;
}
}
return *this;
}
// 日期+天数
Date Date::operator+(int day) const// 因为不改变this指针指向的内容,s
{
Date tmp = *this;
tmp += day;
return tmp;
}
// 日期-天数
Date Date::operator-(int day) const
{
Date tmp(*this);
tmp -= day;
return tmp;
}
// 日期-=天数
Date& Date::operator-=(int day)
{
if (day < 0)
return *this += abs(day);
_day -= day;
while (_day < 1)
{
--_month;
if (_month == 0)
{
_month = 12;
--_year;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
// 前置++
Date& Date::operator++()
{
return *this += 1;
}
// 后置++ // 参数加int只是为了和前置区分开,--同样
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
// 后置--
Date Date::operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
// 前置--
Date& Date::operator--()
{
return *this -= 1;
}
// >运算符重载
bool Date::operator>(const Date& d) const
{
if (_year > d._year)
return true;
else if (_year == d._year && _month > d._month)
return true;
else if (_year == d._year && _month == d._month && _day > d._day)
return true;
return false;
}
// ==运算符重载
bool Date::operator==(const Date& d) const
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
// >=运算符重载
bool Date::operator >= (const Date& d) const
{
return *this > d || *this == d;
}
// <运算符重载
bool Date::operator < (const Date& d) const
{
return !(*this >= d);
}
// <=运算符重载
bool Date::operator <= (const Date& d) const
{
return !(*this > d);
}
// !=运算符重载
bool Date::operator != (const Date& d) const
{
return !(*this == d);
}
// 今天是本年的第几天
static int GetYearDay_toToday(int year, int month, int day)
{
int day1 = 0;
for (int i = 1; i < month; ++i)
{
day1 += GetMonthDay(year, i);
}
day1 += day;
return day1;
}
// 日期-日期 返回天数
int Date::operator-(const Date& d) const
{
int day1 = GetYearDay_toToday(_year, _month, _day);
int day2 = GetYearDay_toToday(d._year, d._month, d._day);
int ret = 0;
if (*this > d)
{
for (int i = d._year; i < _year; ++i)
{
ret += IsLeapYear(i) ? 366 : 365;
}
ret += day1;
ret -= day2;
}
else
{
for (int i = _year; i < d._year; ++i)
{
ret += IsLeapYear(i) ? 366 : 365;
}
ret += day2;
ret -= day1;
}
return ret;
}
10. 类中的const成员
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// d1.Print() : Print(Date* const this) const Date -> Date*
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
const Date d1(2023, 4, 26);
d1.Print();// 报错:不能将“this”指针从“const Date”转换为“Date &” 权限放大
return 0;
}
- 类中,把const修饰的成员函数成为const成员函数。
- const修饰类中的成员函数,实际上const修饰的是成员函数的this指针指向的内容(只修饰this指针)。
const修饰成员函数的写法:
// const修饰成员函数
// const修饰前:void Print(Date* const this)
// const修饰后:void Print(const Date* const this)
void Print() const
{
cout << _year << "/" << _month << "/" << _day << endl;
}
11. 再谈构造函数
前面已经谈过构造函数,构造函数的作用就是初始化对象。下面再仔细分析对象的初始化方式:
11.1 函数体内初始化
函数体内初始化分多种情况,这里不再细说,直接给出最优方式:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
11.2 初始化列表
-
以一个冒号
:
开始 -
后面跟一个数据成员列表(一系列数据成员变量),这些数据成员以逗号
,
分隔 -
每个数据成员都是
成员变量(初始值或表达式)
的格式class Date { public: Date(int year = 1, int month = 1, int day = 1) : _year(year) , _month(month) , _day(day) {} private: int _year; int _month; int _day; };
初始化列表和函数体内初始化并不冲突,也就是说,它们可以共同发挥作用:
class Stack { public: Stack(int capacity = 4) : _capacity(capacity) , _top(0) { _a = (int*)malloc(sizeof(int) * capacity); if (_a == nullptr) { perror("malloc fail"); exit(-1); } } private: int* _a; int _capacity; int _top; };
-
相同的数据成员(相同指的是数据成员中的成员变量相同)在初始化列表中只能出现一次
Date(int year = 1, int month = 1, int day = 1) : _year(year) , _month(month) , _day(day) , _day(day) {} // 会报错
-
初始化列表中的数据成员无顺序先后之分,初始化顺序只与成员变量声明/定义的顺序有关
即成员变量在类中的声明次序即为在初始化列表中初始化次序
class A { public: A(int a = 10) : _b(_a) , _c(_b) , _a(a) {} void Print() { cout << _a << " " << _b << " " << _c << endl; } private: int _a; int _b; int _c; }; // 定义顺序是 _a > _b > _c // 所以,初始化顺序是 _a > _b > _c // 所以,初始化列表中的数据成员的执行顺序实际是_a(a) > _b(_a) > _c(_b)
-
每个成员定义时都要先走一遍初始化列表,无论是否在初始化列表中显式定义
class A { public: A() : _m(10) { _m = 20; } private: int _m; }; int main() { A a;// a中的成员变量_m的值是20 return 0; }
因此,尽量使用初始化列表对对象初始化。
-
给成员变量缺省值,实际也是在初始化列表中定义的
为什么要有初始化列表?
-
初始化const成员变量(const变量只能且必须在定义的时候初始化,之后不能修改)
- 类中的成员变量是在实例化对象时,对类中的成员变量整体进行定义的。在构造函数(函数体)中进行整体定义
- 类中每个成员变量的定义是在初始化列表中定义
class A { public: A() : _n(10) {} private: const int _n;// 这里是声明 };
-
初始化列表中
对于内置类型,有缺省值就初始化为缺省值,没有缺省值就初始化为随机值
对于自定义类型,调用它自己的默认构造函数(没有默认构造函数会报错)
-
初始化自定义类型成员,且该类型没有默认构造函数
class Stack { public: Stack(int capacity) : _capacity(capacity) , _top(_capacity) { _a = (int*)malloc(sizeof(int) * capacity); if (_a == nullptr) { perror("malloc fail"); exit(-1); } } private: int* _a; int _capacity; int _top; }; class Queue { public: Queue() : _popST(4) , _pushST(4) {} private: Stack _pushST;// 声明1 Stack _popST;// 声明2 int _capacity; }; // 如果不在初始化列表显式初始化_pushST,_popST // 声明1和声明2在Queue的初始化列表定义时会调用Stack的默认构造函数,但是没有这种定义方式(无参)相对应的构造函数,因此会报错
-
初始化引用成员变量
原因同const成员变量,都必须在定义时初始化。
11.3 explicit关键字
被explict修饰的函数会禁止传参时的隐式类型转换发生。
C++98规定,“单参”构造函数具有隐式类型转换的功能,“多参”构造函数则不具有(这里的单参指的是允许只传一个参数,即只有一个参数或多个参数但参数要么全缺省要么只有第一个不缺省)。
C++11新增,“多参”构造函数也具有隐式类型转换的功能
class Date
{
public:
// “单参”构造函数传参时,允许隐式类型转换的情况:
// 1. 函数只有一个参数
// 2. 函数有多个参数,但可以只传一个参数(即全缺省或只有第一个参数不全省)
// explicit DateDate(int year = 1, int month = 1, int day = 1) // 禁止隐式类型转换
Date(int year = 1, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
int main()
{
// "单参"构造函数传参时的隐式类型转换:
Date d1 = 2023;// sign
// “多参”构造函数传参时的隐式类型转换:
Date d2 = {2023, 4, 26};
Date d3 = {2023, 4};
// 加权限
Date& d4 = {2023, 4, 26};// 会报错,权限放大:无法从“initializer list”转换为“Date &”
// 右边是const Date(临时变量具有常性,所以是const), 左边是Date
const Date& d5 = {2023, 4, 26};// 这样就可以了
return 0;
}
// 以d1为例
// 按理说,初始化应是:Date d1(2023);
// 但上述代码sign行却直接用对象接收了一个常量整型值
// 实际上是:
// 1. 先用2023构造一个临时Date对象(无名,假如是tmp)
// 2. 再用临时对象tmp拷贝构造d1
// 以上是旧版本编译器的实现机制,新版本编译器(现在大多数编译器)对这个做了优化:
// 将原来的 构造+拷贝构造 优化成了 直接构造
12. static修饰的静态成员
-
static修饰的成员变量称为静态成员变量。
static影响的只是变量的作用域,生命周期都是全局域。
静态成员变量的作用域受类域限制。
-
static修饰的成员函数称为静态成员函数
-
静态成员变量声明时不能给缺省值,必须且只能在类外定义
-
静态成员函数没有隐含的this指针,只能访问静态成员
class A
{
public:
A(int a = 0)
: _a(a)
{
N++;
}
A(const A& aa)
: _a(aa._a)
{
N++;
}
// 无this指针
static int GetN()
{
return N;
}
private:
int _a;
static int N;// N的声明,记录构造函数(包括拷贝构造)被调用的次数
};
int A::N = 0;// N的定义
void Func1(A a)
{}
void Func2(A& a)
{}
A Func3()
{
A a(1);
return a;
}
const A& Func4(const A& a)
{
return a;
}
void Func5(A& a)
{}
int main()
{
A a1(1);
A a2 = 1;
A a3 = a1;
cout << A::GetN() << endl;
Func1(a1);
cout << "void Func1(A a) " << A::GetN() << endl;
Func2(a1);
cout << "void Func2(A& a) " << A::GetN() << endl;
Func3();
cout << "A Func3() " << A::GetN() << endl;
Func4(a1);
cout << "const A& Func4(const A& a) " << A::GetN() << endl;
Func5(a1);
cout << "void Func5(A& a) " << A::GetN() << endl;
return 0;
}
// 输出
3
void Func1(A a) 4
void Func2(A& a) 4
A Func3() 6
const A& Func4(const A& a) 6
void Func5(A& a) 6
13. 友元
友元提供了一种不受封装引起的访问权限受限的方式。但这样会增加耦合度,破坏了封装的特性,因此不建议多用。
友元分为友元函数和友元类。
13.1 友元函数
之前在运算符重载那里已经简单论述过
class Date
{
friend operator==(const Date& d);// 友元声明,可以放到类中的任意位置
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
bool operator==(const Date& d) // 友元声明过的函数,可以直接访问类中的任何成员,不受访问权限的限制
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
特点:
- 友元函数可以直接访问类里的私有和保护成员,但它不是类的成员函数
- 友元函数不能用const修饰(const修饰的实际是this指针,但友元函数没有this指针)
- 友元函数可以在类中的任意位置声明
- 一个函数可以是多个类的友元函数
13.2 友元类
class Time
{
// 声明友元类
friend class Date;// 在Time类中声明友元类Date,那么在类Date中就可直接访问Time类的对象中的私有成员
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTime(int hour, int minute, int second)// 直接访问Time类对象_t的私有成员
{
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
特点:
-
友元关系不能传递
例:A是B的友元,B是C的友元,但A不是C的友元
-
友元关系是单向的。
例:上述代码中,Date是Time的友元,但Time不是Date的友元
14. 内部类(了解即可)
其实就是嵌套类,在一个类中定义另一个类。
class A
{
private:
int _a;
public:
class B
{
private:
int _b;
};
};
-
上述代码中,B是内部类,A是外部类
-
内部类和外部类是独立的两个类。
cout << sizeof(A) << endl;// 输出 4 // 类A中只有一个成员变量_a
-
只是内部类的访问受A的类域和访问限定符的限制。
-
受类域限制
A::B b;// 定义B类
-
受访问限定符限制
class A { private: int _a; class B { private: int _b; }; }; int main() { A::B b;// 报错 无法访问 private class(在“A”类中声明) return 0; }
-
-
内部类是外部类的友元类(但外部类不是内部类的友元)
15. 匿名对象
也可称之为无名对象。
class A
{
public:
A(int a = 10)
: _a(a)
{
cout << "A(int)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
// 有名对象
A a1;
A a2(20);
// 下面两个都是匿名对象
A();
A(1);
return 0;
}
-
匿名对象的生命周期仅是匿名对象所在那一行。
匿名对象的实用性:
有时候我们不想创建对象,只是想用类中的某个函数,这时就可以用创建匿名对象来调用类中的函数。
class Solution
{
public:
Solution(int a = 10)
: _a(a)
{}
void FuncSolution(int a = 100)
{
cout << a << endl;
}
private:
int _a;
};
int main()
{
/*Solution s;
s.FuncSolution();*/
Solution().FuncSolution();// 这一行结束就销毁
return 0;
}
16. 拷贝对象时编译器的一些优化
// 情景一:
class A
{
public:
A(int a = 10)
: _a(a)
{
cout << "A(int a = 10)" << endl;
}
A(const A& a)
{
_a = a._a;
cout << "A(A& a)" << endl;
}
private:
int _a;
};
void Func(A aa)
{}
int main()
{
A a(1);
Func(a);// code-1
cout << "-------" << endl;
Func(A(1));// code-2
return 0;
}
// 输出
A(int a = 10)
A(A& a)
-------
A(int a = 10)
// 按理说,code-1和code-2都应是 构造+拷贝构造
// 但是,输出结果却是:
// code-1:构造+拷贝构造
// code-2:构造
// 这就是编译器对构造的优化。
// 但这个优化是有条件限制的,正如上述代码展示的那样,只有构造+拷贝构造是连续的才会优化,举个例子:
// code-2:参数A(1)在这一行代码走完就被销毁了,在后面的代码中不会被用到,会被优化:构造+拷贝构造 -> 构造
// code-1:参数a之后可能还会被用到,因此不会优化
// 情景2:
A Func1()
{
A a;
return a;
}
int main()
{
A aa = Func1();// code-1
cout << "------" << endl;
A aaa;
aaa = Func1();// code-2
return 0;
}
// 输出
A(int a = 10)
A(A& a)
------
A(int a = 10)
A(int a = 10)
A(A& a)
// 按理说上述代码code-1输出应是:构造+拷贝构造+拷贝构造
// 实际结果却是:构造+拷贝构造(被优化掉的是code-1那一行的拷贝构造)
//
// code-2处是赋值重载,不会被优化
// 因此code-2的输出结果是main函数中的构造+Func1()中的构造+拷贝构造,即构造+构造+拷贝构造
//
// 所以,应尽量采用code-1处的写法
// 情景3:
A Func2()
{
return A(1);
}
int main()
{
A a = Func2();// code-1
return 0;
}
// 输出
A(int a = 10)
// 按理说,上述代码输出应是Func2()中的构造+拷贝构造+code-1处的拷贝构造
// 但编译器对其优化成了直接用Func2()中的1构造code-1处的a,即构造
综上,只有当构造和拷贝构造连续,或可理解为在一个步骤上时,即构造或拷贝构造得到的对象后面不会再被用到,才会触发编译器的优化机制。