1. 面对对象设计概述
1) 面对对象和面向过程
C语言是面向过程
的,关注的是实现过程
,分析出求解问题的步骤,通过函数调用逐步解决问题
C++是面向对象
的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成
2) 类的引入
在C语言中的结构体中只能定义基本数据类型,这就不能表达一个对象的行为,而在C++中的结构体就可以通过定义函数(方法)来表达行为
例如学生有年龄,学号,姓名等等,其行为有写作业,睡觉等等
struct Student {
char name[20];
int age;
int id;
void doHomework() {
cout << name << "正在做作业" << endl;
}
void sleep() {
cout << name << "正在睡觉" << endl;
}
};
int main() {
Student s;
strcpy(s.name, "小明");
s.age = 18;
s.id = 20;
s.doHomework();
s.sleep();
}
而在C++通常使用class代替
3) 类的定义
// 创建类
class ClassName{
field; // 字段(属性) 或者 成员变量
method; // 行为 或者 成员方法
};
类的两种定义方式:
- 声明和定义在类中实现(注: 成员函数如果在类中定义,编译器可能会将其当成内联函数处理)
- 声明在.h文件中,类的定义放在.cpp文件中
注:
::
在CPP中表示作用域和所属关系,有三种使用方法
- 作用域符号
使用方法: 类名::类成员名称
这是C++为了避免不同的类中的类成员相同的情况发生,根据类名划分作用域从而避免冲突
- 全局作用域符号
使用方法: ::变量名
当全局变量名和局部变量名冲突时,就可以使用
::全局变量名
指明使用实体
- 作用域分解运算符
使用方法: 类名:: 类方法名
在.h中声明一个类A定义一个函数void f() ,然后想要在.cpp中定义里面的函数,就可以使用void A::f()表明f()是类A的成员函数
4) 类的访问限定符及封装
a.类的访问修饰限定符
C++实现封装的方用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用
说明:
- public修饰的成员在类外可以访问
- protected和private修饰的成员在类外不能直接被访问
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- class的默认访问权限为private,struct为public(因为struct要兼容C)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
面试题:
问题:C++中struct和class的区别是什么?
解答:C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类。和class是定义类是一样的,区别是struct的成员默认访问方式是public,class是的成员默认访问方式是 private
b.封装
【面试题】 面向对象的三大特性:封装、继承、多态。
在类和对象阶段,我们只研究类的封装特性,那什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理:我们如何管理兵马俑呢?比如如果什么都不管,兵马俑就被随意破坏了。那么我们首先建了一座房子把兵马俑给封装起来。但是我们目的全封装起来,不让别人看。所以我们开放了售票通道,可以买票突破封装在合理的监管机制下进去参观。类也是一样,我们使用类数据和方法都封装到一下。不想给别人看到的,我们使用protected/private把成员封装起来。开放一些共有的成员函数对成员合理的访问。所以封装本质是一种管理。
5) 类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
cout<<_name<<" "_gender<<" "<<_age<<endl;
}
6) 类的实例化
用类类型创建对象的过程,称为类的实例化
- 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它 (所以即使一个类中没有任何函数和变量,其大小也不是0, 而是1)
- 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间
7) 类对象模型
对于一个类其大小是多少呢?在内存中是如何分配空间呢?
- 对象中包含类的各个成员
缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。那么如何解决呢?
-
只保存成员变量,成员函数存放在公共的代码段
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ytibYyrd-1665386091529)(C:\Users\a\AppData\Roaming\Typora\typora-user-images\image-20221005205832907.png)]
问题:对于上述两种存储方式,那计算机到底是按照那种方式来存储的?
// 类中既有成员变量,又有成员函数
class A1 {
public:
void f1(){}
private:
int _a;
};
// 类中仅有成员函数
class A2 {
public:
void f2() {}
};
// 类中什么都没有---空类
class A3
{};
int main() {
cout <<"A1: " << sizeof(A1) << " A2: " << sizeof(A2) << " A3: " << sizeof(A3) << endl;
}
结论:一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类
对齐规则:
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
- 注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值 (VS中默认的对齐数为8 )
- 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
8) this指针
问题1: 在日期类中如果成员变量和形参名字相同,该如何正确访问呢?
class Date {
private:
int year;
int _mouth;
int _day;
public:
void Init(int year, int mouth, int day) {
// 名字相同,修改的是形参,不会修改成员变量
year = year;
_mouth = mouth;
_day = day;
}
void Print() {
cout << _year << "-" << _mouth << "-" << _day << endl;
}
};
int main() {
Date d1;
d1.Init(2022, 10, 1);
d1.Print();
}
可以使用作用域访问修饰符
Date::year = year;
但是在通常情况下,还是要用_
表示成员变量
问题2: 前面已经了解到类成员变量是独特的存储空间,而成员函数是是公用的,那么类是如何找到实例对象呢?
int main() {
Date d1;
d1.Init(2022, 10, 1);
d1.Print();
Date d2;
d2.Init(2022, 10, 21);
d2.Print();
}
// 这里调用不同对象的相同成员函数,通过反汇编可以知道是同一个函数(因为call的地址相同)
实际上在调用函数时,会默认传递一个该对象的地址,而函数使用this指针指向这个对象
class Date {
private:
int _year;
int _mouth;
int _day;
public:
//void Init(Date* const this, int year, int mouth, int day)
void Init(int year, int mouth, int day) {
_year = year;
_mouth = mouth;
_day = day;
this->Print();
}
void Print() {
cout << _year << "-" << _mouth << "-" << _day << endl;
}
//void Print(Date* const this) {
// cout <<this-> _year << "-" << this->_mouth << "-" << this->_day << endl;
//}
};
int main() {
Date d1;
d1.Init(2022, 10, 1); // d1.Init(&d1, 2022, 10, 1);
d1.Print(); // d1.Print(&d1);
Date d2;
d2.Init(2022, 10, 21); //d2.Init(&d2, 2022, 10, 21);
d2.Print(); // d1.Print(&d2);
}
this指针特性:
- this指针的类型:类类型* const
- 不能类外部使用,类函数中使用,只能在成员函数内部使用
- this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针
- this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
面试题:
- this指针存在哪里?
一般是在栈(形参)中,有些编译器会放到寄存器中,例如vs2019放到ecx中
// 1.下面程序能编译通过吗?
// 2.下面程序会崩溃吗?在哪里崩溃
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
void Show()
{
cout<<"Show()"<<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
p->Show();
}
p->PrintA();会运行崩溃, p->Show();会正常执行
解释:
p指针虽然是空指针,但是p调用成员函数不会出现空指针访问,因为成员函数没有存放在对象里,这里的this指针是空指针传递给show函数,但是它并没有解引用,所以不会出错,而在PrintA()函数内部其实是this->_a就会出现访问空指针,所以会崩溃
2. 类特性解释
1) 类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数
class Date {};
a.构造函数
对于下面的日期类,有年月日这些成员属性,在需要实例化对象时,通过Init()函数去设置默认值是十分麻烦,特别是有多个对象需要实例化时,于是C++就提供了构造函数
方便初始化成员变量
class Date {
private:
int _year;
int _mouth;
int _day;
public:
void Init(int year, int mouth, int day) {
_year = year;
_mouth = mouth;
_day = day;
}
void Print() {
cout << _year << "-" << _mouth << "-" << _day << endl;
}
};
int main() {
Date d1;
d1.Init(2022, 10, 1);
d1.Print();
Date d2;
d2.Init(2022, 10, 21);
d2.Print();
}
构造函数的特性
- 是一个特殊的成员函数,其名字与类名相同,创建类类型对象时由编译器自动调用
- 生命周期: 只在实例化对象时调用
- 无返回值
- 可以重载
class Date { public : // 1.无参构造函数 Date () {} // 2.带参构造函数 Date (int year, int month , int day ) { _year = year ; _month = month ; _day = day ; } private : int _year ; int _month ; int _day ; }; void TestDate() { Date d1; // 调用无参构造函数 Date d2 (2015, 1, 1); // 调用带参的构造函数 // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明 // 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象 Date d3(); }
- 在类中如果没有显式的定义构造函数,那么编译器会默认提供一个无参的构造函数
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,在语法层面上可以同时存在,但是在使用时只能使用一个, (因为会造成二义性)
// 默认构造函数 class Date { public: Date() { _year = 1900 ; _month = 1 ; _day = 1; } Date (int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private : int _year ; int _month ; int _day ; }; void Test() { Date d1; // 这里会报错,因为对于全缺省和默认构造函数都可以满足无参的条件,造成二义性 Date d2(2002,1,1); Date d3(2001,3); Date d4(2001); // d2,d3,d4是可以满足缺省参数的条件 }
对日期类的默认构造函数调用后其成员变量的值仍然是默认值没有改变,那么默认构造函数没有用吗?
其实C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型:如 int/char…,自定义类型就是我们使用class/struct/union自己定义的类型,在日期类中添加自定义类型A后就可以查看其初始化过程了
class Date { private: int _year; int _mouth; int _day; A _aa; }; class A { public: A() { cout << "A()" << endl; } private: int _a; }; int main() { Date d1(); }
最终打印出A(),说明默认构造函数是可以初始化自定义类型,当然,如果A类没有默认构造器就会报错
- 任意一个类的默认构造函数有三个: 全缺省,无参,编译器提供的默认构造器
b.析构函数
概念: 与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象的一些资源清理工作(比如对象molloc的空间)
特性:
- 析构函数也是特殊的成员函数
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值
- 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数
- 对象生命周期结束时,C++编译系统系统自动调用析构函数
class Stack { public: ~Stack() { cout << "~Stack()" << endl; } private: int* _stack; size_t _capacity; size_t _top; }; int main() { Stack s1; Stack s2; }
- 和构造函数相似,析构函数对于内置类型成员变量不做处理,对于自定义类型成员变量回去调用它的析构函数.
//例如用两个栈实现队列中,MyQueue的生命周期结束时,就会去调用pushST和popST的析构函数,MyQueue类就不必去处理内存泄漏,而对于内置类型内部成员变量会随着栈帧的销毁而消失. class MyQueue { public: void push(){ } //.... private: Stack pushST; Stack popST; } class Stack { public: ~Stack() { free(_stack); _stack = nullptr; _capacity = 0; _top = 0; } private: int* _stack; size_t _capacity; size_t _top; }; int main() { Stack s1; Stack s2; }
c. 拷贝构造函数
概念: 只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
特性:
拷贝构造函数是构造函数的一个重载形式
拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //1. 由于实参是形参的一份临时拷贝,这里使用的是传值方式就会发生拷贝,但是d不是内置类型可以直接赋值,它是自定义类型,所以它会去想要一份拷贝,就会去调用拷贝构造函数,从而发生无限递归. // Date(Date d) //2. 所以正确做法是传引用,就不是传值方式,而是d1的引用 // Date(Date& d) //3. 最好使用const修饰,增强代码的健壮性,因为有时候会出现下面的情况,导致出错 Date(Date& d) { //写反了 d._year = _year; _month = d._month; _day = d._day; } // 如果使用const就能够及时发现并改正 Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; } private: int _year; int _month; int _day; }; int main() { Date d1; // 传入的是对象 Date d2(d1); return 0; }
若未显示定义,系统生成默认的拷贝构造函数
对于内置类型是按照字节序一个一个拷贝的(浅拷贝)
对于自定义类型就不能使用默认的拷贝构造函数,自己实现(深拷贝)
例如下面的栈
class Stack { public: Stack(int capacity = 10) { _stack = (int*)malloc(sizeof(int) * capacity); } Stack(const Stack& s) { _stack = s._stack; _capacity = s._capacity; _top = s._top; } ~Stack() { free(_stack); _stack = nullptr; _capacity = 0; _top = 0; } private: int* _stack; size_t _capacity; size_t _top; }; int main() { // 创建一个容量为10的栈s1 Stack s1(10); // 拷贝一个相同的栈s2 Stack s2(s1); //导致问题: // 1. 由于是浅拷贝,s1和s2的栈指针都是指向同一个内存地址,如果后面对任意一个栈操作都会同步影响另一个栈中的数据 //2. 在函数结束时会自动触发析构函数,析构顺序和出栈顺序一样,所以是s2先析构,当s2析构完成后栈空间就被回收了,s1中又去析构,但是刚才s1已经把空间还给操作系统了,就会去找这个空间,虽然找到了,但是这个空间不是s1的啦,所以会报错,对于这些存在指针的复制要重新开空间后复制,不能够是简单的复制. }
2) 赋值运算符重载
a. 运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似 .
函数名: 关键字operator后面接需要重载的运算符符号
函数原型:返回值类型 operator操作符(参数列表)
注:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型或者枚举类型的操作数
- 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
- .* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载
// 全局的operator== class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //private: 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(2018, 9, 26); Date d2(2018, 9, 27); cout << (d1 == d2) << endl; }
因为要访问成员变量,但是它是私有的,如果要在类外访问,可以由类提供get()函数或者破坏封装性把成员变量访问权限改成公有.
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } // bool operator==(Date* this, const Date& d2) // 这里需要注意的是,左操作数是this指向的调用函数的对象 bool operator==(const Date& d2) { return _year == d2._year && _month == d2._month && _day == d2._day; } private: int _year; int _month; int _day; }; int main() { Date d1(2018, 9, 26); Date d2(2018, 9, 27); cout << (d1 == d2) << endl; d1.operator==(d2); }
如果写在类内部则需要考虑this指针,所以只需要传另一个需要比较的对象,
注: 如果全局和类中都含有相同的运算符重载函数,在vs中就会先去类中寻找
b.赋值运算符重载
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
//返回尽量传引用,如果传值会调用拷贝复制函数,而使用引用在多个对象间相互赋值时可以减少调用拷贝复制函数,提高效率
Date& operator=(const Date& d) {
//判断是否自己给自己赋值,使用地址的比较,
//如果使用*this != d 会出现2个问题
// 1. != 符号没有重载,要自己实现
// 2. 对象中的成员变量的比较效率低
if (this != &d) {
// 下面三句代码完成两个对象的赋值
_year = d._year;
_month = d._month;
_day = d._day;
}
// 返回当前对象的引用
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
Date d2(2018, 9, 28);
// 两个已经存在的对象之间是赋值拷贝
d2 = d3 = d1;
// 用一个存在的对象去赋值拷贝一个马上实例化的对象是拷贝构造
Date d4 = d2;
Date d5(d1);
}
特点: 赋值拷贝和拷贝构造相似,对于内置类型是按照字节序的拷贝,对于自定义类型是调用其默认的赋值拷贝
3) 日期类的实现
Date.h
#pragma once
class Date
{
public:
//全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1);
// 析构函数
~Date();
// 拷贝构造函数
Date(const Date& d);
// 获取当前日期的天数
int GetMonthDay(int year, int month);
// 打印当前日期
void Print();
//赋值运算符重载
Date& operator=(const Date& d);
// 日期 += 天数
Date & operator+=(int day);
// 日期+天数
Date operator+(int day);
// 日期-天数
Date operator-(int day);
// 日期-=天数
Date& operator-=(int day);
// 前置++
Date& operator++();
// 后置++
Date operator++(int);
// 后置--
Date operator--(int);
// 前置--
Date& operator--();
// >运算符重载
bool operator>(const Date& d);
// ==运算符重载
bool operator==(const Date& d);
// >=运算符重载
inline bool operator >= (const Date& d);
// <运算符重载
bool operator < (const Date& d);
// <=运算符重载
bool operator <= (const Date& d);
// !=运算符重载
bool operator != (const Date& d);
// 日期-日期 返回天数
int operator-(const Date& d);
// 当前日期是星期几
void PrintWeekDay();
private:
int _year;
int _month;
int _day;
};
Date.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include "Date.h"
#include<iostream>
using namespace std;
// 在声明时写了缺省值后,这里不可以写,否则会报"无法重新定义默认参数"
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
// 判断当前日期是否合法
if (!(year >= 0 && (month > 0 && month < 13) && (day > 0 && GetMonthDay(year, month) <= 365)))
{
cout << "非法日期->" ;
Print();
}
}
Date::~Date()
{
_year = _month = _day = 0;
}
Date::Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
int Date::GetMonthDay(int year, int month) {
static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
int day = days[month];
if (month == 2
&& ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
day += 1;
}
return day;
}
void Date::Print() {
cout << _year << '-' << _month << '-' << _day << endl;
}
Date& Date::operator=(const Date& d) {
if (this != &d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
Date& Date::operator+=(int 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) {
Date ret(*this);
ret += day;
return ret;
}
Date& Date::operator-=(int day) {
if (day < 0)
{
return *this += -day;
}
_day -= day;
while (_day <= 0) {
--_month;
if (_month == 0)
{
_month = 12;
_year--;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day) {
Date ret(*this);
ret -= day;
return ret;
}
//前置++
Date& Date::operator++() {
*this += 1;
return *this;
}
// 后置++,为了区分前置++用int做占位符
Date Date::operator++(int) {
Date ret(*this);
*this += 1;
return ret;
}
// 后置--
Date Date::operator--(int) {
*this -= 1;
return *this;
}
// 前置--
Date& Date::operator--() {
Date ret(*this);
*this -= 1;
return *this;
}
// >运算符重载
bool Date::operator>(const Date& d) {
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;
}
else {
return false;
}
}
// ==运算符重载
bool Date::operator==(const Date& d) {
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
// >=运算符重载
inline bool Date::operator >= (const Date& d) {
return (*this == d) || (*this > d);
}
// <运算符重载
bool Date::operator < (const Date& d) {
return !(*this >= d);
}
// <=运算符重载
bool Date::operator <= (const Date& d) {
return (*this == d) || (*this < d);
}
// !=运算符重载
bool Date::operator != (const Date& d) {
return !(*this == d);
}
// 日期-日期 返回天数
int Date::operator-(const Date& d) {
Date max = *this;
Date min = d;
int flag = 1;
if (max < min) {
max = d;
min = *this;
flag = -1;
}
int count = 0;
while (min != max) {
++min;
++count;
}
return count * flag;
}
void Date::PrintWeekDay() {
// 选取第一个星期1(起始日期)
Date start(1900, 1, 1);
int count = *this - start;
// 或者使用匿名对象
// int count = *this - Date(1900, 1, 1);
const char* week[] = { "星期一","星期二","星期三","星期四","星期五","星期六","星期天" };
cout << week[count % 7] << endl;
}
4) const 成员
对于一个日期对象调用Print()函数,虽然没有显式的传参,但是默认是把对象地址传递this指针,但是如果对象被const修饰后,就会出现权限变大的错误,在Print()函数的默认参数是Date* const this
这里的const修饰的是this指针,但是d2对象传参的类型是const Date*
修饰的是 *this
,所以出错.
int main() {
Date d1(2022,1,2);
d1.Print();// 实参: &d1 -> 形参类型 Date*
const Date d2;
d2.Print();// 实参: &d2 -> 形参类型 const Date*
}
此时就可以使用const修饰成员函数,实际修饰该成员函数隐含的this指针**,表明在该成员函数中**不能对类的任何成员进行修改,
其实最终该成员函数就被双重const修饰,权限变小,this和*this都不能被修改
5) 取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
}
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容
3. 类特性再理解
1) 构造函数
在创建对象,编译器调用构造函数初始化成员变量
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
虽然构造函数可以对成员变量初始化,但是不能将其称作为类对象成员的初始化构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因初始化只能初始化一次,而构造函数体内可以多次赋值
所以C++提供初始化类成员变量的方法初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
初始化列表的作用:
每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
成员变量中如果存在以下成员,必须放在初始化列表位置进行初始化
引用成员变量
const成员变量
自定义类型成员变量(没有默认构造函数)
原因: 对于引用变量和const变量必须声明和定义同时进行,也就是必须初始化,在类成员变量相当于声明,而我们初始化过程就是定义.对于自定义类型之前讲解过会在初始化时调用其构造函数,如果这里没有默认构造函数就需要传参初始化
class A { public: A(int a) :_a(a) {} private: int _a; }; class Date { public: Date(int year, int month, int day, int N, int ref, int aa) : _year(year) , _month(month) , _day(day) ,_N(N) ,_ref(ref) ,_aa(aa) {} private: int _year; int _month; int _day; const int _N; // const int& _ref; // 引用 A _aa; // 自定义类型 };
尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化
class A { public: A(int a = 0) { _a = a; cout << "A()构造函数" << endl; } A(const A& aa) { _a = aa._a; cout << "A()拷贝构造" << endl; } A& operator = (const A& aa) { _a = aa._a; cout << "A()=运算符重载" << endl; return *this; } private: int _a; }; class Date { public: // 构造函数 Date(int year, int month, int day, const A& aa) { _aa = aa; _year = year; _month = month; _day = day; } // 初始化列表 //Date(int year, int month, int day, A aa) // : _year(year) // , _month(month) // , _day(day) // ,_aa(aa) //{} private: int _year; int _month; int _day; A _aa; // 自定义类型 }; int main() { A a(10); Date d1(2001, 1, 1, a); }
使用构造函数使用初始化列表
总结: 对于自定义类型使用构造函数和初始化列表都可以不影响效率,而使用自定义类型时,构造函数的效率低(在传参时调用第二次构造函数,然后是运算符重载),而初始化列表只需要进行一次拷贝构造即可,所以建议在有自定义类型成员变量时,使用初始化列表方式.
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
class A { public: A(int a) :_a1(a) , _a2(_a1) {} void Print() { cout << _a1 << " " << _a2 << endl; } private: int _a2; int _a1; }; int main() { A aa(1); aa.Print(); }
- 因为
_a2
比_a1
先声明,在初始化时_a2
先初始化,所以这里的_a2
被一个未初始化的_a1
赋值结果是一个随机值,随后_a1
被初始化为1.所以结果为 1 随机值
2) explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用
class Date
{
public:
Date(int year)
:_year(year)
{
cout << "Date()" << endl;
}
//explicit Date(int year)
// :_year(year)
//{}
private:
int _year;
int _month;
int _day;
};
int main ()
{
Date d2 = 2020;
}
上述代码用一个整型去初始化一个Date类型的对象,并且还成功赋值了,这是为什么呢?
在C语言中学习过隐式类型转换
double d = 1.1; // 因为double和int都是表示数据大小只是精度和大小不同,C语言需要通过截断来初始化 // 这个过程是 d先被截断后保存在一个临时空间,然后再把数据赋值给i int i = d;
同样的这里是利用2020作为构造函数的参数,利用它构造一个临时对象,然后把这个对象赋值给d2.
注: 在有些编译器会对这两个过程优化成一次构造.可以验证
所以可以使用explicit修饰构造函数,禁止单参构造函数的隐式转换 ,提高代码可读性
3) static 关键字
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态的成员变量一定要在类外进行初始化
应用场景: 面试题:实现一个类,计算中程序中创建出了多少个类对象
class A { public: A() { ++_count; } A(const A& t) { ++_count; } static int GetACount() { return _count; } private: static int _count; }; int A::_count = 0; int main() { cout <<"创建前: " << A::GetACount() << endl; A a1, a2; A a3(a1); cout << "创建后: "<< A::GetACount() << endl; return 0; }
使用说明:
- 静态成员为所有类对象所共用,不属于某个实例对象
- 静态成员变量必须在类外定义(即初始化),
- 静态类成员可以使用
类名::静态成员
的方式访问,或者对象名.静态成员
- 因为静态成员函数没有隐藏的
this指针
,所以在函数体中不能使用任何非静态成员- 静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值
4) C++11成员初始化新方法
因为在C++98中创建对象时,会对自定义类型调用其默认的构造函数,而内置类型不做处理,所以在C++11中引入新方法:
C++11支持非静态成员变量在声明时进行初始化赋值但是要注意这里不是初始化,这里是给声明的成员变量缺省值
说明: 在声明时并没有初始化,而是在创建对象时调用默认构造函数或者调用了但是没有初始化这个变量,就会使用这个缺省值完成成员变量的初始化.
class B
{
public:
B(int b = 0)
:_b(b)
{}
int _b;
};
class A
{
public:
void Print()
{
cout << a << endl;
cout << b._b << endl;
cout << p << endl;
cout << arr[0] << endl;
}
private:
// 非静态成员变量,可以在成员声明时给缺省值。
int a = 10;
B b = 20;
int* p = (int*)malloc(4);
int arr[5] = {1,2,3,4,4};
static int n;
};
int A::n = 10;
int main()
{
A a;
a.Print();
return 0;
}
5) 友元
友元分为:友元函数和**友元类
**友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用
a. 友元函数
引入问题; 实现重载operator<<
分析: 我们没办法将operator<<重载成成员函数。因为cout是双目运算符,并且cout的输出流对象和隐含的this指针在抢占第一个参数的位置。导致this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以我们要将operator<<重载成全局函数。但是这样的话,又会导致类外没办法访问成员,那么这里就需要友元来解决。operator>>同理
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
Date() {
}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
int main()
{
Date d1(2022,1,1);
Date d3;
cin >> d3;
cout << d3 << endl;
cout << d1 << endl;
return 0;
}
说明:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用和原理相同
b. 友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员
- 友元关系是单向的,不具有交换性
- 友元关系不能传递
class Date; // 前置声明
class Time
{
// 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
friend class Date;
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 = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
6) 内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限
注意:内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元
特性:
- 内部类可以定义在外部类的public、protected、private都是可以的
- 内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名
- sizeof(外部类)=外部类,和内部类没有任何关系
class A { private: static int k; int h; public: class B { public: void foo(const A& a) { cout << k << endl;//OK cout << a.h << endl;//OK } }; }; int A::k = 1; int main() { A::B b; b.foo(A()); return 0; }
修饰**
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用和原理相同
b. 友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员
- 友元关系是单向的,不具有交换性
- 友元关系不能传递
class Date; // 前置声明
class Time
{
// 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
friend class Date;
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 = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
6) 内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限
注意:内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元
特性:
- 内部类可以定义在外部类的public、protected、private都是可以的
- 内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名
- sizeof(外部类)=外部类,和内部类没有任何关系
class A { private: static int k; int h; public: class B { public: void foo(const A& a) { cout << k << endl;//OK cout << a.h << endl;//OK } }; }; int A::k = 1; int main() { A::B b; b.foo(A()); return 0; }