文章目录
- C++入门篇3(类和对象【重点】)
-
- 1、面向过程和面向对象
- 2、类的引入
- 3、类的定义
- 4、类的访问限定符及封装
-
- 4.1、访问限定符
- 4.2、封装
- 5、类的作用域
- 6、类的实例化(对象)
- 7、类对象模型
-
- 7.1、类对象的存储方式
- 7.2、结构体(类)内存对齐规则
- 8、this指针
-
- 8.1、this指针的引出
- 8.2、this指针的特性
- 8.3、C语言和C++实现Stack的对比
- 9、类的6个默认成员函数
- 10、构造函数
-
- 10.1、构造函数概念
- 10.2、构造函数的特性
- 11、析构函数
-
- 11.1、析构函数概念
- 11.2、析构函数特性
- 12、拷贝构造函数
-
- 12.1、拷贝构造函数概念
- 12.2、拷贝构造函数的特性
- 13、赋值运算符重载
-
- 13.1、运算符重载
- 13.2、赋值运算符重载
- 13.3、前置++和后置++重载
- 14、日期类的实现
- 15、const成员
-
- 15.1、const修饰成员变量
- 15.2、const修饰成员函数
- 16、取地址及const取地址操作符重载
- 17、初始化列表(重点)
-
- 17.1、初始化列表概念
- 17.2、初始化列表特性
- 17.3、构造函数的隐式类型转换
- 17.4、explicit关键字
- 18、static成员
-
- 18.1、static成员概念
- 18.2、static成员特性
- 19、友元
-
- 19.1、友元函数
- 19.2、友元类
- 20、内部类
-
- 20.1、内部类概念
- 20.2、内部类特性
- 21、匿名对象
- 22、构造和拷贝构造时编译器的一些优化
C++入门篇3(类和对象【重点】)
1、面向过程和面向对象
- 面向过程是一种以过程为中心的编程思想,它将数据和处理数据的方法封装在一起,形成一种相互依存的整体——过程。在面向过程的编程中,程序的执行流程是由用户在使用中决定的。它通常按功能划分为若干个基本模块,这些模块形成一个树状结构,各模块之间的关系尽可能简单,在功能上相对独立。
- 举例:比如汽车维修,可以将其看作一个问题,然后按照一系列的步骤来解决,如检查引擎、更换零件、修复电路等。每个步骤都是一个函数,按照顺序执行。
- 面向对象则是一种更高级的编程思想,它将数据和操作数据的方法封装在一起,形成一个相互依存的整体——对象。在面向对象的编程中,程序流程由对象之间的消息传递决定。同类对象抽象出其共性,形成类。类中的大多数数据只能用本类的方法进行处理。类通过一个简单的外部接口与外界发生关系,对象与对象之间通过消息进行通信。
- 举例:比如洗衣机,可以将其看作一个对象,具有“洗衣服”等方法,人作为另一个对象,具有“加洗衣粉”、“加水”等方法。然后通过对象之间的消息传递,执行“人.加洗衣粉”、“人.加水”、“洗衣机.洗衣服”等操作,完成洗衣过程。
2、类的引入
-
C语言中,结构体只能定义变量,而在C++中,结构体可以声明定义变量,还可以声明定义函数。 比如:之前在数据结构初阶中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现,会发现
struct
中也可以定义函数。#include using namespace std; typedef int DataType; struct Stack { void Init(size_t capacity) { _array = (DataType *) malloc(sizeof(DataType) * capacity); if (nullptr == _array) { perror("malloc申请空间失败"); return; } _capacity = capacity; _size = 0; } void Push(const DataType &data) { // 扩容 _array[_size] = data; ++_size; } DataType Top() { return _array[_size - 1]; } void Destroy() { if (_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; } } DataType *_array; size_t _capacity; size_t _size; }; int main() { Stack s; s.Init(10); s.Push(1); s.Push(2); s.Push(3); cout
上面的结构体,C++一般使用class来代替
3、类的定义
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
class
为定义类的关键字,ClassName
为类的名字,{}
中为类的主体,注意类定义结束时后面分号不能省略。
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
类的两种定义方式:
-
声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
//栈 class Stack { void Init(size_t capacity) { _array = (DataType *) malloc(sizeof(DataType) * capacity); if (nullptr == _array) { perror("malloc申请空间失败"); return; } _capacity = capacity; _size = 0; } void Push(const DataType &data) { // 扩容 _array[_size] = data; ++_size; } DataType Top() { return _array[_size - 1]; } void Destroy() { if (_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; } } DataType *_array; size_t _capacity; size_t _size; };
-
类声明放在
.h
文件中,成员函数定义放在.cpp
文件中,注意:成员函数名前需要加类名::
。//Stack.h文件 class Stack { public: void Init(size_t capacity); void Push(const DataType &data); DataType Top(); void Destroy(); DataType *_array; size_t _capacity; size_t _size; };
//Stack.cpp文件 #include "Stack.h" void Stack::Init(size_t capacity) { _array = (DataType *) malloc(sizeof(DataType) * capacity); if (nullptr == _array) { perror("malloc申请空间失败"); return; } _capacity = capacity; _size = 0; } void Stack::Push(const DataType &data) { // 扩容 _array[_size] = data; ++_size; } DataType Stack::Top() { return _array[_size - 1]; } void Stack::Destroy() { if (_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; } }
一般情况下,采用第二种方式。
成员变量命名规则建议:在变量名前加_
。
如:
class Date {
public:
void Init(int year) {
_year = year;
}
private:
int _year;
};
4、类的访问限定符及封装
4.1、访问限定符
在面向对象编程中,类的访问限定符用于控制类成员的访问权限。以下是常见的访问限定符:
public
:public
成员可以从任何位置访问,包括类的外部和所有从该类派生的子类。
protected
:protected
成员只能从其本身、其子类以及其友元类中访问。
private
:private
成员只能从其本身和其友元类中访问。这三种访问级别为封装和信息隐藏提供了工具。在创建类时,应尽可能使更多的成员为
private
或protected
,从而限制对这些成员的访问。这样,就可以在不破坏封装性的前提下改变这些成员的实现在一些情况下,如果希望某些成员仅能从特定的派生类中访问,这时可以使用protected
访问级别。而public
成员则应尽可能少用,它们应仅用于那些真正需要公开的成员。
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 如果后面没有访问限定符,作用域就到
}
即类结束。class
的默认访问权限为private
,struct
为public
(因为struct
要兼容C)。- 注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
4.2、封装
在C++中,封装是面向对象编程的三大特性(封装、继承、多态)之一,它是一种将数据(变量)和操作数据的函数捆绑在一起作为一个单独的对象(类)的技术。通过封装,可以隐藏类的内部实现细节,只通过类提供的公共接口来访问类中的成员。这有助于保护对象的状态和维护对象的行为。
封装主要通过类和成员函数来实现。类是对象的蓝图,它定义了对象的数据和行为。成员函数是类的成员,它们定义了如何操作类的数据。通过将数据和操作数据的函数捆绑在一起,可以创建一个具有特定功能和行为的对象。
通过合理使用访问限定符,可以更好地控制类的访问权限,保护类的内部状态和行为不被外部代码随意修改。
举例:假设你是一名自行车设计师,你的工作是设计并生产自行车。你的客户群体是自行车爱好者,他们关心的是自行车的性能、外观和价格。为了满足客户的需求,你需要将自行车的设计、生产和销售过程封装起来,使得客户只需要关心自行车的性能、外观和价格,而不需要关心背后的生产过程。
5、类的作用域
-
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用
::
作用域操作符指明成员属于哪个类域。class Person { public: void PrintPersonInfo(); private: char _name[20]; char _gender[3]; int _age; }; // 这里需要指定PrintPersonInfo是属于Person这个类域 void Person::PrintPersonInfo() { cout
6、类的实例化(对象)
-
用类类型创建对象的过程,称为类的实例化。
-
类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;比如:入学时填写的学生信息表,表格就可以看成是一个类,来描述具体学生信息。
类就像谜语一样,对谜底来进行描述,谜底就是谜语的一个实例。
谜语:“年纪不大,胡子一把,主人来了,就喊妈妈” 谜底:山羊 -
一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。
class Person { public: void showInfo(); public: int _age;//这里是声明,不是定义,所以不占用空间 char *_name; char *_sex }; void Test() { //Person._age = 100; // 编译失败:error C2059: 语法错误:“.” Person man;//实例化一个对象,占用物理空间 man._age = 10; man._name = "hh"; man._sex = "nan"; man.showInfo(); }
做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象(建好的房子)才能实际存储数据,占用物理空间。
-
7、类对象模型
7.1、类对象的存储方式
只保存成员变量,成员函数存放在公共的代码段
7.2、结构体(类)内存对齐规则
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员类型里最大的那个类型 的较小值。
VS中默认的对齐数为8
- 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
-
计算下面程序的类的大小:
// 类中既有成员变量,又有成员函数 class A1 { public: void f1() {} private: int _a; }; // 类中仅有成员函数 class A2 { public: void f2() {} }; // 类中什么都没有---空类 class A3 { }; int main() { cout
结论:一个类的大小,实际就是该类中“成员变量”之和,当然要注意内存对齐。
注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
8、this指针
8.1、this指针的引出
-
我们先来定义一个日期类
Date
class Date { public: void Init(int year, int month, int day) { //void Init( Date* const this, int year, int month, int day) _year = year; _month = month; _day = day; //this->_year = year; //this->_month = month; //this->_day = day; } void Print() { // void Print(Date* const this) cout _year _month _day
-
这里我们注意到,
Date
类实例化了两个对象d1
和d2
,d1
和d2
都调用了Init
函数和Print
函数,那么这是怎么做到两个对象传递的数据不会混淆呢?其实是this指针帮我们解决的这个问题,对于函数void Init(int year, int month, int day);
,其实它是隐含了this指针的,相当于是void Init( Date* const this, int year, int month, int day);
,那么代码d1.Init(2022, 1, 11);
就相当于是d1.Init(&d1, 2022, 1, 11);
。函数Print
也一样,隐含了this
指针。有了this
指针之后,将d1
和d2
分别传给它们的this指针,把数据分别给它们的成员变量。注意:这里
&d1
和this
不能显式给出,但是this
可以显式使用。 -
this
指针出处:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数(this
),让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
-
8.2、this指针的特性
以下是
this
指针的主要特性:
- 只能在成员函数中使用:
this
指针只能在类的成员函数中使用,不能在全局函数、静态成员函数或其他函数中使用。- 指向调用对象:
this
指针指向调用对象本身,也就是当前对象。在成员函数中,可以通过this
指针来访问对象的成员变量或成员函数。- 构造和析构:在成员函数中,
this
指针是在成员函数的开始前构造,并在成员函数的结束后清除。- 存储位置:
this
指针的存储位置取决于编译器,可能是堆栈、寄存器或全局变量,这取决于编译器的实现。- 类型:
this
指针的类型是类==类型* const
,也就是说,它是一个指向常量对象的指针,也就是this
指针不可以修改指向(除了传参的时候(初始化的时候))==。- 隐含的形参:
this
指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。- 无法获取位置:由于
this
指针只有在成员函数中才有定义,所以获得一个对象后,不能通过对象使用this
指针,所以也就无法知道一个对象的this
指针的位置。不过,可以在成员函数中指定this
指针的位置。
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A {
public:
void Print() {
cout Print();//正常运行
return 0;
}
// 2.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A {
public:
void PrintA() {
cout PrintA();//运行崩溃
return 0;
}
- 为什么第一个程序正常运行?因为p是类A的类对象指针,那么代码
p->Print();
相当于p->Print(p)
,那么函数void Print();
里面this
指针就指向nullptr
,虽然传过去的是空指针,但是,this
并没有访问对象(也就是让this
初始化指向空了)。所以程序正常运行。 - 为什么第二个程序运行崩溃?与第一个程序不同的是,类A的成员函数
Print
里面访问了成员变量_a
,那么也就是访问this->_a
,但是此时this
已经是nullptr
了,所以不能再去访问成员变量_a
了。因此程序运行崩溃。
8.3、C语言和C++实现Stack的对比
-
C语言实现:
typedef int DataType; typedef struct Stack { DataType *array; int capacity; int size; } Stack; void StackInit(Stack *ps) { assert(ps); ps->array = (DataType *) malloc(sizeof(DataType) * 3); if (NULL == ps->array) { assert(0); return; } ps->capacity = 3; ps->size = 0; } void StackDestroy(Stack *ps) { assert(ps); if (ps->array) { free(ps->array); ps->array = NULL; ps->capacity = 0; ps->size = 0; } } void CheckCapacity(Stack *ps) { if (ps->size == ps->capacity) { int newcapacity = ps->capacity * 2; DataType *temp = (DataType *) realloc(ps->array, newcapacity * sizeof(DataType)); if (temp == NULL) { perror("realloc申请空间失败!!!"); return; } ps->array = temp; ps->capacity = newcapacity; } } void StackPush(Stack *ps, DataType data) { assert(ps); CheckCapacity(ps); ps->array[ps->size] = data; ps->size++; } int StackEmpty(Stack *ps) { assert(ps); return 0 == ps->size; } void StackPop(Stack *ps) { if (StackEmpty(ps)) return; ps->size--; } DataType StackTop(Stack *ps) { assert(!StackEmpty(ps)); return ps->array[ps->size - 1]; } int StackSize(Stack *ps) { assert(ps); return ps->size; } int main() { Stack s; StackInit(&s); StackPush(&s, 1); StackPush(&s, 2); StackPush(&s, 3); StackPush(&s, 4); printf("%dn", StackTop(&s)); printf("%dn", StackSize(&s)); StackPop(&s); StackPop(&s); printf("%dn", StackTop(&s)); printf("%dn", StackSize(&s)); StackDestroy(&s); return 0; }
可以看到,在用C语言实现时,
Stack
相关操作函数有以下共性:-
每个函数的第一个参数都是
Stack*
-
函数中必须要对第一个参数检测,因为该参数可能会为
NULL
-
函数中都是通过
Stack*
参数操作栈的 -
调用时必须传递
Stack
结构体变量的地址(所以每次都需要开辟一个指针的大小的空间存储指针)
结构体中只能定义存放数据的结构,操作数据的方法不能放在结构体中,即数据和操作数据的方式是分离开的,而且实现上相当复杂一点,涉及到大量指针操作,稍不注意可能就会出
错。
-
-
C++实现:
typedef int DataType; class Stack { public: void Init() { _array = (DataType *) malloc(sizeof(DataType) * 3); if (NULL == _array) { perror("malloc申请空间失败!!!"); return; } _capacity = 3; _size = 0; } void Push(DataType data) { CheckCapacity(); _array[_size] = data; _size++; } void Pop() { if (Empty()) return; _size--; } DataType Top() { return _array[_size - 1]; } int Empty() { return 0 == _size; } int Size() { return _size; } void Destroy() { if (_array) { free(_array); _array = NULL; _capacity = 0; _size = 0; } } private: void CheckCapacity() { if (_size == _capacity) { int newcapacity = _capacity * 2; DataType *temp = (DataType *) realloc(_array, newcapacity * sizeof(DataType)); if (temp == NULL) { perror("realloc申请空间失败!!!"); return; } _array = temp; _capacity = newcapacity; } } private: DataType *_array; int _capacity; int _size; }; int main() { Stack s; s.Init(); s.Push(1); s.Push(2); s.Push(3); s.Push(4); printf("%dn", s.Top()); printf("%dn", s.Size()); s.Pop(); s.Pop(); printf("%dn", s.Top()); printf("%dn", s.Size()); s.Destroy(); return 0; }
C++中通过类可以将数据 以及 操作数据的方法进行完美结合,通过访问权限可以控制哪些方法在类外可以被调用,即封装,在使用时就像使用自己的成员一样,更符合人类对一件事物的认知。而且每个方法不需要传递
Stack*
的参数了,编译器编译之后该参数会自动还原,即C++中Stack *
参数是编译器维护的,C语言中需用用户自己维护。
9、类的6个默认成员函数
-
如果一个类里面什么都没有,如
class A{};
,我们叫这个类为空类。但是这个空类里面真的什么都没有吗?并不是。任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。 -
默认成员函数:默认成员函数指的是在类中,用户没有显式实现,编译器自动为所有对象实例化时提供默认行为的特殊成员函数。
10、构造函数
10.1、构造函数概念
-
构造函数是一种特殊的函数,它用于初始化一个对象的数据成员。当创建一个类的对象时,构造函数会自动被调用,以初始化对象的数据成员。构造函数可以带有参数,也可以没有参数。如果没有提供任何参数,那么它就是默认构造函数。
class Date { public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout
这里我们看到这个
Date
类初始化是用Init
来实现的,但是我们C++是提供了默认初始化的函数的(默认构造函数),可以让我们省去新写一个函数的时间。构造函数概念:构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
10.2、构造函数的特性
-
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
-
构造函数的特性包括:
- 函数名与类名相同。
- 无返回值。
- 构造函数可以重载,即在类内可以编写多个参数不同的构造函数。
- 构造函数在对象构造阶段所调用,用于初始化和赋初值。
- 构造函数在对象整个生命周期内只调用一次。
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
- 如果没有显式实现构造函数,编译器会自动生成默认构造函数,但自定义类型必须调用构造函数进行初始化(后面演示)。如果已经显示实现了构造函数,则编译器不会生服务器托管网成默认构造函数,这时候调用构造函数初始化就得传参数了。
class Date { public: // // 如果用户显式定义了构造函数,编译器将不再生成 // Date(int year, int month, int day) { // _year = year; // _month = month; // _day = day; // } void Print() { cout
- 关于编译器生成的默认成员函数,很多童鞋会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?
d1
对象调用了编译器生成的默认构造函数,但是d1
对象_year/_month_day
,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??
解答:C++把类型分成==内置类型(基本类型)和自定义类型==。内置类型就是语言提供的数据类型,如:int/char...
,自定义类型就是我们使用class/struct/union
等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对**自定类型成员_t
**调用的它的默认成员函数,如果这个自定义类型没有默认构造函数,那么就需要用到初始化列表(下面会讲),不过这就相当于套娃,因为这个自定义类型的默认构造函数也需要初始化(不是随机值的那种,下面会讲缺省值等方法)。
class Time { public: Time() { cout
注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
class Time { public: Time() { cout
-
构造函数在对象整个生命周期内只调用一次。无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
class Date { public: //默认构造函数(无参构造函数) // Date() { // _year = 1; // _month = 1; // _day = 1; // } // //默认构造函数(全缺省构造函数) // Date(int year = 1, int month = 1, int day = 1) { // _year = year; // _month = month; // _day = day; // } //我们没写编译器默认生成的构造函数,对应下面的缺省成员变量,但是如果有构造函数(非默认)存在(你已经定义了自己的构造函 //数,编译器就不会再自动生成默认构造函数),还想要调用默认构造函数的话,需要声明函数 Date(){},否则调用默认构造函数 //编译器会找不到默认构造函数 void Print() { cout
class Date { public: //Date(){} //存在非默认构造函数时候,配合下面缺省成员变量使用 Date(int year, int month, int day) { //你已经定义了自己的构造函数,编译器就不会再自动生成默认构造函数 _year = year; _month = month; _day = day; } void Print() { cout
11、析构函数
11.1、析构函数概念
析构函数是C++中的一种特殊成员函数,它与构造函数相反,用于执行一些清理任务,通常用于释放分配给对象的内存空间等。
析构函数名是在类名前加上字符
~
,它没有参数和返回值。一个类有且只有一个析构函数,如果用户没有显式定义,系统会自动生成默认的析构函数。在对象销毁时,会自动调用析构函数,完成类的一些资源清理工作。
11.2、析构函数特性
-
析构函数的特性包括:
- 析构函数与类名相同,但它前面必须加上波浪号
~
,用以与构造函数相区别。 - 析构函数没有返回类型,甚至不能声明为void类型。
- 析构函数没有参数,因此不能被重载。
- 对象的生命周期结束时,系统自动调用析构函数。
typedef int DataType; class Stack { public: Stack(size_t capacity = 3) { _array = (DataType *) malloc(sizeof(DataType) * capacity); if (NULL == _array) { perror("malloc申请空间失败!!!"); return; } _capacity = capacity; _size = 0; } void Push(DataType data) { // CheckCapacity(); _array[_size] = data; _size++; } // 其他方法... ~Stack() { if (_array) { free(_array); 服务器托管网 _array = NULL; _capacity = 0; _size = 0; } } private: DataType *_array; int _capacity; int _size; }; void TestStack() { Stack s; s.Push(1); s.Push(2); } int main(){ TestStack(); return 0; }
- 编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
class Time { public: ~Time() { cout
-
默认析构函数通常用于释放==对象在其生命周期中分配的资源,例如内存、文件句柄等。当对象==不再使用时,系统会自动调用默认析构函数,以释放对象所占用的资源(如果对象动态申请了内存,那么就只释放对象,这个申请的内存不一定释放,可能造成内存泄露)。
需要注意的是,如果类中没有显式定义析构函数,系统会自动生成一个默认析构函数。但是,如果类中分配了动态内存或创建了其他需要清理的资源,那么显式定义析构函数是必要的,以便正确释放这些资源(比如Stack类)。
-
对象对构造函数和析构函数的调用顺序:
局部对象和全局对象的构造函数和析构函数的调用顺序是不同的。
对于局部对象,它们的构造函数将在它们被创建时立即调用,而析构函数将在它们所在的作用域结束时被调用。也就是说,当程序离开该作用域时,局部对象的析构函数将被调用。
如果一个作用域内定义了多个局部对象,那么它们的构造函数将按照它们在代码中定义的顺序依次被调用。同样地,它们的析构函数也将按照相反的顺序被调用,即最后创建的局部对象将首先被销毁。
对于全局对象,它们的构造函数将在程序开始时被调用,而析构函数将在程序结束时被调用。因此,全局对象的构造函数和析构函数的调用顺序与程序的生命周期相同。
对于静态对象:分为静态局部对象和静态全局对象。静态局部对象调用构造函数(顺序和局部对象一样)只会在程序第一次调用此函数时调用一次,调用析构函数是在函数调用结束时被调用。静态全局对象调用构造函数:在程序中的所有函数(包括main函数)执行之前调用,调用析构函数是在main函数执行完毕或调用exit函数时被调用。
需要注意的是,全局对象可以被定义在多个文件中。如果全局对象在不同的文件中定义,那么每个文件中全局对象的构造函数将按照它们在文件中定义的顺序依次被调用。同样地,每个文件中全局对象的析构函数也将按照它们在文件中定义的顺序依次被调用。因此,在编写多文件程序时,需要注意确保全局对象在所有相关的文件中都被正确地初始化和销毁。
-
下列程序对象的构造函数和析构函数的调用顺序是?
C c; int main() { A a; B b; static D d; return 0; }
答案:构造函数:C A B D,析构函数: B A D C
分析:
- 类的析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意
static
对象的存在,因为static
改变了对象的生存作用域,需要等待程序结束时才会析构释放对象 - 全局对象先于局部对象进行构造
- 局部对象按照出现的顺序进行构造,无论是否为
static
- 所以构造的顺序为 C A B D
- 析构的顺序按照构造的相反顺序析构,只需注意
static
改变对象的生存作用域之后,会放在局部对象之后进行析构 - 因此析构顺序为B A D C
- 类的析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意
-
- 析构函数与类名相同,但它前面必须加上波浪号
12、拷贝构造函数
12.1、拷贝构造函数概念
拷贝构造函数是一种特殊的构造函数,它用于创建一个对象的副本。在C++中,拷贝构造函数用于复制一个对象,以便在程序中进行各种操作。拷贝构造函数的形参必须是引用,且形参必须是只有一个当前类对象(单形参),但并不限制为
const
,一般普遍的会加上const
限制。此函数经常用在函数调用时用户定义类型的值传递及返回。如果没有显式地定义拷贝构造函数,编译器会生成一个默认的拷贝构造函数,这个默认的拷贝构造函数会简单地复制每个数据成员的值。如果显式地定义了拷贝构造函数,编译器就不会生成默认的拷贝构造函数,程序员需要自己实现拷贝构造函数以确保正确地复制对象的数据。在拷贝构造函数中,通常需要将源对象的数据成员的值复制到新对象中。特别地,如果类中包含指针成员,需要确保拷贝出来的对象不与源对象共享这些指针所指向的内存(防止被析构两次)。
12.2、拷贝构造函数的特性
-
拷贝构造函数是一种特殊的构造函数,其特性包括:
-
拷贝构造函数是构造函数的一个重载形式。
-
拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } // Date(const Date& d) // 正确写法 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 Time { public: Time() { _hour = 1; _minute = 1; _second = 1; } Time(const Time &t) { _hour = t._hour; _minute = t._minute; _second = t._second; cout
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
-
拷贝构造函数用于创建一个新的对象,并将其初始化为另一个已存在的对象的副本。
-
拷贝构造函数通常需要将源对象的数据成员的值复制到新对象中。特别地,如果类中包含指针成员,需要确保拷贝出来的对象不与源对象共享这些指针所指向的内存,即需要完成深拷贝,得自己重写拷贝构造函数。
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。 typedef int DataType; class Stack { public: Stack(size_t capacity = 10) { _array = (DataType *) malloc(capacity * sizeof(DataType)); if (nullptr == _array) { perror("malloc申请空间失败"); return; } _size = 0; _capacity = capacity; } void Push(const DataType &data) { // CheckCapacity(); _array[_size] = data; _size++; } ~Stack() { if (_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; } } private: DataType *_array; size_t _size; size_t _capacity; }; int main() { Stack s1; s1.Push(1); s1.Push(2); s1.Push(3); s1.Push(4); Stack s2(s1); return 0; }
这里
s1
和s2
的_array
指向同一块空间,那么当调用结束后,会调用两次析构函数(s1
和s2
分别一次),则_array
指向的空间就被释放两次,第二次释放的时候就会报错(第一次释放后这块空间就不属于_array
了)。所以解决办法就是不使用默认的拷贝构造函数,自己来写拷贝构造函数,让这个
s1
和s2
的_array
不指向同一个空间(s2
的_array
重新开辟一块空间,这就是深拷贝,也就是啥变量空间都重新搞一次)。typedef int DataType; class Stack { public: Stack(size_t capacity = 10) { _array = (DataType *) malloc(capacity * sizeof(DataType)); if (nullptr == _array) { perror("malloc申请空间失败"); return; } _size = 0; _capacity = capacity; } Stack (const Stack& st){ this->_array = (DataType *) malloc(st._capacity * sizeof(DataType)); if (nullptr == this->_array) { perror("malloc申请空间失败"); return; } this->_size = st._size; this->_capacity = st._capacity; } void Push(const DataType &data) { // CheckCapacity(); _array[_size] = data; _size++; } ~Stack() { cout
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
-
拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
class Date { public: Date(int year, int minute, int day) { cout
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
-
13、赋值运算符重载
13.1、运算符重载
C++中的运算符重载是指将已存在的运算符赋予新的含义,从而改变它的行为。在C++中,我们可以通过重载运算符来定义自己的操作规则,使得这些运算符能够适用于自定义的数据类型。
运算符重载的语法格式为:
返回类型 operator 运算符(参数列表) { // 操作代码 }
其中,返回类型可以是任何有效的数据类型,包括基本数据类型和自定义数据类型。运算符可以是C++中已存在的任何运算符,包括算术运算符、比较运算符、逻辑运算符等。参数列表可以是零个或多个参数,具体取决于运算符的定义。
注意:运算符重载需要注意以下几点:
除了类属关系运算符”
.
“、成员指针运算符”.*
“、作用域运算符”::
“、sizeof
运算符和三目运算符”? :
“以外,C++中的所有运算符都可以重载。重载运算符限制在C++语言中已有的运算符范围内的允许重载的运算符之中,不能创建新的运算符。
运算符重载实质上是函数重载,因此编译程序对运算符重载的选择,遵循函数重载的选择原则。
重载之后的运算符不能改变运算符的优先级和结合性,也不能改变运算符操作数的个数及语法结构。
运算符重载不能改变该运算符用于内部类型对象的含义。它只能和用户自定义类型的对象一起使用,或者用于用户自定义类型的对象和内部类型的对象混合使用时。
运算符重载是针对新类型数据的实际需要对原有运算符进行的适当的改造,重载的功能应当与原有功能相类似,避免没有目的地使用重载运算符。
重载=运算符时容易忘记写返回值。
重载赋值运算符时,记得加
const
,因为赋值操作必须是固定的右值。重载时,写在类中的只能有一个参数(实际有两个参数,另外一个是
this
指针,我们看不见而已),需要两个参数的时候,要写在类外,用友元在类内声明(作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的
this
)。重载递增运算符时,要注意哪个要加引用,哪个不用加引用。
// 全局的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; } void Test() { Date d1(2018, 9, 26); Date d2(2018, 9, 27); cout (d1 == d2) endl; } int main() { Test(); return 0; }
// 类里面的operator== class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //干脆重载成成员函数。 //bool operator==(const Date* this,const Date &d2) //隐含了this bool operator==(const Date &d2) { return this->_year == d2._year && _month == d2._month && _day == d2._day; } private: int _year; int _month; int _day; }; void Test() { Date d1(2018, 9, 26); Date d2(2018, 9, 27); cout (d1 == d2) endl;//流插入运算符优先级比重载的运算符 == 高 ,所以加括号提升优先级 //d1 == d2 --> d1.operator==(d2) cout d1.operator==(d2) endl;//相当于d1.operator==(&d1,d2); } int main() { Test(); return 0; }
13.2、赋值运算符重载
-
赋值运算符重载格式:
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
-
返回
*this
:支持连续赋值(a = b = c)
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* this, const Date &d) Date &operator=(const Date &d) { if (this != &d) {//防止自己给自己赋值 _year = d._year; _month = d._month; _day = d._day; } return *this;//支持连续赋值 } private: int _year; int _month; int _day; }; void Test() { Date d1(2018, 9, 26); Date d2(2018, 9, 27); d1 = d2;//d1 = d2 --> d1.operator=(d2) d1.operator=(d2);//相当于 d1.operator=(&d1,d2); } int main(){ Test(); return 0; }
-
赋值运算符只能重载成类的成员函数不能重载成全局函数
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } int _year; int _month; int _day; }; // 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数 Date &operator=(Date &left, const Date &right) { if (&left != &right) { left._year = right._year; left._month = right._month; left._day = right._day; } return left; } // 编译失败: // error C2801: “operator =”必须是非静态成员
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
-
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值(也需要在自定义类型类里看情况要不要实现这个赋值运算符)。
class Time { public: Time() { _hour = 1; _minute = 1; _second = 1; } //这里都是自定义类型赋值,其实可以不用自己实现 Time &operator=(const Time &t) { if (this != &t) { _hour = t._hour; _minute = t._minute; _second = t._second; } return *this; } private: int _hour; int _minute; int _second; }; class Date { private: // 基本类型(内置类型) int _year = 1970; int _month = 1; int _day = 1; // 自定义类型 Time _t; }; int main() { Date d1; Date d2; d1 = d2; return 0; }
既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。 typedef int DataType; class Stack1 { public: Stack1(size_t capacity = 10) { _array = (DataType *) malloc(capacity * sizeof(DataType)); if (nullptr == _array) { perror("malloc申请空间失败"); return; } _size = 0; _capacity = capacity; } void Push(const DataType &data) { // CheckCapacity(); _array[_size] = data; _size++; } ~Stack1() { if (_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; } } private: DataType *_array; size_t _size; size_t _capacity; }; int main() { Stack1 s1; s1.Push(1); s1.Push(2); s1.Push(3); s1.Push(4); Stack1 s2; s2 = s1; return 0; }
解决办法:和拷贝构造函数一样。让这个
s1
和s2
的_array
不指向同一个空间(s2
的_array
重新开辟一块空间,这就是深拷贝,也就是啥变量空间都重新搞一次)。//解决办法 typedef int DataType; class Stack1 { public: Stack1(size_t capacity = 10) { _array = (DataType *) malloc(capacity * sizeof(DataType)); if (nullptr == _array) { perror("malloc申请空间失败"); return; } _size = 0; _capacity = capacity; } //让这个s1和s2的_array不指向同一个空间(s2的_array重新开辟一块空间,这就是深拷贝,也就是啥变量空间都重新搞一次) Stack1& operator=(const Stack1 &st) { this->_array = (DataType *) malloc(st._capacity * sizeof(DataType)); if (nullptr == this->_array) { perror("malloc申请空间失败"); exit(-1); } this->_size = st._size; this->_capacity = st._capacity; return *this; } void Push(const DataType &data) { // CheckCapacity(); _array[_size] = data; _size++; } ~Stack1() { if (_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; } } private: DataType *_array; size_t _size; size_t _capacity; }; int main() { Stack1 s1; s1.Push(1); s1.Push(2); s1.Push(3); s1.Push(4); Stack1 s2; s2 = s1; return 0; }
注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
13.3、前置++和后置++重载
- 前置
++
先+1
再计算 - 后置
++
先计算再+1
-
前置
++
运算符重载规定:无参数 -
后置
++
运算符重载规定:参数加一个int
类型
class Date {
public:
//默认构造函数,与下面全缺省成员变量配合使用
Date() {
}
Date(int year, int month, int day) { //你已经定义了自己的构造函数,编译器就不会再自动生成默认构造函数
_year = year;
_month = month;
_day = day;
}
// 前置++ 加完原来的值也改变
// *this就是d1
Date &operator++() {
_day += 1;
return *this;
}
Date operator++(int ){ //后置++运算符重载就是这样规定的,参数加个int类型
Date temp(*this);
_day += 1;
return temp;//临时对象,不能引用
}
private:
int _year = 1900;
int _month = 1;
int _day = 1;
};
int main() {
Date d1;
Date d;
d = ++d1;//d1.operator++(&d1);
d = d1++;
return 0;
}
14、日期类的实现
// Date.h 文件
#include
using namespace std;
class Date {
public:
// 获取某年某月的天数
int 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;
}
// 全缺省的构造函数
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);
// 日期-天数
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);
// >=运算符重载
bool operator>=(const Date &d);
//
// Date.cpp 文件
#include "Date.h"
// 全缺省的构造函数
//声明和定义分离,需要指定类域
Date::Date(int year, int month, int day) {
if (year >= 0 && (month >= 1 && month = 0 && (month >= 1 && month d2.operator=(&d2, d3)
Date &Date::operator=(const Date &d) {
if (this != &d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
// 析构函数
Date::~Date() {
// cout GetMonthDay(_year, _month)) {
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13) {
_month = 1;
_year++;
}
}
return *this;
}
// 日期+天数 -- 不改变原值
Date Date::operator+(int day) {
Date temp(*this);
temp += day;
return temp;
}
// 日期-=天数 -- 改变原值
Date &Date::operator-=(int day) {
//如果输入的day小于0
if (day 运算符重载
bool Date::operator>(const Date &d) {
if (_year >= d._year) {
if (_year > d._year)
return true;
else {
//_year == d._year
if (_month >= d._month) {
if (_month > d._month)
return true;
else {
//_month == d._month
if (_day >= d._day) {
if (_day > d._day)
return true;
else
return false;
}
}
}
}
}
return false;
}
// ==运算符重载
bool Date::operator==(const Date &d) {
return _year == d._year && _month == d._month && _day == d._day;
}
// >=运算符重载
bool Date::operator>=(const Date &d) {
return (*this > d) || (*this == d);
}
// = d);
}
//
// main.cpp 文件
#include "Date.h"
//Date类的实现
int main() {
Date d;
Date d1(2023, 10, 27);
Date d2;
Date d3(2100, 7, 22);
// d2 += 10;
// d3 = d2 + 10;
//
// d2 -= 10;
// d3 = d2 - 10;
d1 -= 100;
d1 -= -100;
cout (d1 - d2) endl;
return 0;
}
15、const成员
15.1、const修饰成员变量
const
关键字用于修饰成员函数或成员变量。当一个成员变量被声明为const
时,它表示这个成员变量的值不能被修改。class Date { public: void operator++() { (this->x)++; //(this->y)++;//y是const成员变量,不能修改 } private: int x; // 非const变量 const int y = 1; // const变量,const变量必须初始化 };
15.2、const修饰成员函数
当一个成员函数被声明为
const
时,它表示这个成员函数不能修改类的任何non-const
(非const)成员变量。这有助于确保数据的一致性和安全性。其中const
修饰的是*this
,,如下面代码,那么隐含的this
的类型就是const Date* const this
。class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } bool operator++() const{//相当于bool operator++(const Date* const this) //(*this)._year++;//报错,const修饰的是*this,所以*this不能改变 } private: int _year; // 非const变量 int _month; int _day; const int y = 1; // const变量 };
思考下列程序的结果?
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout "Print()" endl; cout "year:" _year endl; cout "month:" _month endl; cout "day:" _day endl endl; } void Print() const { cout "Print()const" endl; cout "year:" _year endl; cout "month:" _month endl; cout "day:" _day endl endl; } private: int _year; // 年 int _month; // 月 int _day; // 日 }; void Test() { Date d1(2022, 1, 13); d1.Print();//Print() const Date d2(2022, 1, 13);//Printf()const d2.Print(); } int main(){ Test(); return 0; }
请思考下面的几个问题:
const对象可以调用非const成员函数吗?
答:不可以,权限只能缩小或者平移,不能变大。
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } bool operator==(const Date &d) { //相当于bool operator==(Date* const this, const Date &d) return _year == d._year && _month == d._month && _day == d._day; } private: int _year; // 非const变量 int _month; int _day; const int y = 1; // const变量 }; int main() { const Date d1(2022, 1, 1); const Date d2(2022, 1, 2); //int ret = d1 == d2;//报错,这里const对象传给this,this的类型是Date* const而不是 const Date* ,因为*this才是对象,this是指针 //cout return 0; }
解决办法:将该成员函数用const修饰。
//解决办法 class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } bool operator==(const Date &d) const{ return _year == d._year && _month == d._month && _day == d._day; } private: int _year; // 非const变量 int _month; int _day; const int y = 1; // const变量 }; int main() { const Date d1(2022, 1, 1); const Date d2(2022, 1, 2); int ret = d1 == d2; cout ret endl; return 0; }
非const对象可以调用const成员函数吗?
答:可以。权限可以缩小
//权限缩小 class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } bool operator==(const Date &d) const{ return _year == d._year && _month == d._month && _day == d._day; } private: int _year; // 非const变量 int _month; int _day; const int y = 1; // const变量 }; int main() { Date d1(2022, 1, 1); Date d2(2022, 1, 2); int ret = d1 == d2;//这里非const调用const cout ret endl; return 0; }
const成员函数内可以调用其它的非const成员函数吗?
答:可以。虽然非
const
成员函数可能会改变类的成员变量,但它们不能改变const
成员函数的局部变量(这个局部变量是const类型,如果有的话),也不能改变const
成员函数所引用的成员变量(但是可以修改局部对象,因为这个类型不是const)。class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } bool operator++() { (*this)._year++; } int jj(int x){ return ++x; } bool operator==(const Date &d) const{ Date a(11,1,1); ++a; int b = 1; //int c= jj(this,b);//报错,jj不是const成员函数 return _year == d._year && _month == d._month && _day == d._day; } private: int _year; // 非const变量 int _month; int _day; const int y = 1; // const变量 };
非const成员函数内可以调用其它的const成员函数吗?
答:可以。const关键字表示这个成员函数不会修改类的任何数据成员,因此在非const成员函数内调用不会有什么风险。
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } bool operator==(const Date &d) const { return _year == d._year && _month == d._month && _day == d._day; } bool operator++() { Date aa(2022, 1, 1); *this == aa; (*this)._year++; } private: int _year; // 非const变量 int _month; int _day; const int y = 1; // const变量 };
总结:1、权限可以平移或者缩小,不能放大。
2、const成员函数内可以调用其它的非const成员函数。
3、非const成员函数内可以调用其它的const成员函数。
16、取地址及const取地址操作符重载
-
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
class Date { public : Date() : _year(1), _month(1), _day(1) { //初始化列表,后面马上会讲 _year = 2; _month = 3; _day = 4; } Date *operator&() { cout "operator&()" endl; return this; } const Date *operator&() const { cout "operator&() const" endl; return this; } private : int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { Date d1, d2; const Date d3; cout &d1 endl &d2 endl &d3 endl; return 0; }
-
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!(比如误导别人)
17、初始化列表(重点)
首先,在之前学到的知识我们知道,对象是在构造函数里初始化的,比如下面这个场景。
class Date { public : Date(int year, int month, int day) { _year = year; _month = month; _day = day; } private : int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { Date d1(2023,11,3); return 0; }
我们注意到这里每次初始化都需要在对象调用构造函数的时候给值,如果有时候我们忘了怎么办?有人会说,使用默认构造函数初始化,如下:
class Date { public : Date() { _year = 1; _month = 1; _day = 1; } private : int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { Date d1; return 0; }
或者使用全缺省的默认构造函数或者采用缺省的成员变量,这些都可以。但是考虑一个问题,如果要初始化的是一个引用、一个常成员变量、或者一个没有默认构造函数的自定义类型成员变量。阁下又该如何应对呢?仅使用上述的构造函数不能解决这个问题。那么就需要初始化列表来处理了。
17.1、初始化列表概念
初始化列表是C++中的一个概念,用于在构造函数初始化期间,对类的成员变量进行初始化。初始化列表主要分为两种:成员变量初始化列表和构造函数初始化列表(上述构造函数
{}
里初始化)。成员变量初始化列表在类定义中,以冒号开头,后跟一系列以逗号分隔的初始化字段。这些初始化的成员变量在对象创建时,会按照初始化列表中==声明(一般是private访问限定符下的成员变量的声明顺序)==的顺序进行初始化。
class Date {
public :
Date(int year, int month, int day) : _year(year), _month(month), _day(day) {//初始化列表
}
private :
int _year; // 年
int _month; // 月
int _day; // 日
};
int main() {
Date d1(2023, 11, 3);
return 0;
}
17.2、初始化列表特性
- 类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量
- const成员变量
- 自定义类型成员(且该类没有默认构造函数时)
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次),构造函数里面可以进行赋值(初始化)(上述三个成员不能在构造函数里面初始化)。
class Time {
public:
Time(int hour, int minute, int second) {
cout "Time(int ,int , int )" endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date {
public :
Date(int year, int month, int day) : _year(year), _month(month), _day(day), _n(year), _x(1), _t(1, 1, 1) {
}
private :
int _year; // 年
int _month; // 月
int _day; // 日
int &_n;//引用
const int _x;//const成员变量
Time _t;//没有默认构造函数的自定义类型成员变量
};
int main() {
Date d1(2023, 11, 3);
return 0;
}
-
尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
class Time { public: Time(int hour = 0) : _hour(hour) { cout "Time()" endl; } private: int _hour; }; class Date { public: Date(int day) { cout "Date()" endl; } private: int _day; Time _t; }; int main() { Date d(1);//先Time() 再 Date() }
-
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
class A { public: A(int a) : _a1(a), _a2(_a1) {} void Print() { cout _a1 " " _a2 endl;//a2先声明,但是并没有初始化, } private: int _a2; int _a1; }; int main() { A aa(1);// 1 随机值 aa.Print(); }
17.3、构造函数的隐式类型转换
构造函数不仅可以构造与初始化对象,对于单个参数或者半缺省或者全缺省的构造函数,还具有类型转换的作用。
class Date {
public:
// Date(int year)
// : _year(year) {}
Date(int year, int month = 1, int day = 1)
: _year(year), _month(month), _day(day) {}
Date &operator=(const Date &d) {
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(11);
// 用一个整型变量给日期类型对象赋值
// 实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值
d1 = 2023;//2023先转换为Date类型,然后用2023构造一个无名对象,相当于 d1 = d2(2023)
d1 = (2021,2,1); //逗号表达式的值是最后一个数值(这里是1) 相当于 d1 = d3(1)
d1 = {2021, 2, 1};//相当于 d1 = d4(2021, 2, 1)
return 0;
}
我们可以注意到,这里给给多参数的构造函数进行隐式类型转换,传参可以使用
{}
。
-
需要注意的是,需要隐式类型转换的变量类型必须和构造函数的参数类型匹配。
class Date { public: // Date(int year) // : _year(year) {} //这里验证构造函数的隐式类型转换功能 --- 需要隐式类型转换的变量类型必须和构造函数的参数类型匹配 Date(int *n) {} Date(int year, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {} Date &operator=(const Date &d) { if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; } private: int _year; int _month; int _day; }; int main() { int *p = nullptr; Date d1(2023, 11, 6); d1 = p;//这里p进行隐式类型转换了,先创建一个临时对象,再将临时对象赋值给d1 int** num = nullptr; //d1 = num;//报错,这里num和构造函数的参数类型不匹配 Date d2 = {2023, 11, 6}; const Date &d3 = {2023, 11, 6}; return 0; }
17.4、explicit关键字
-
用explicit修饰构造函数,将会禁止构造函数的隐式转换。
class Date { public: // 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用 // explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译 // explicit Date(int year) // : _year(year) {} // 2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转换作用 // explicit修饰构造函数,禁止类型转换 explicit Date(int year, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {} Date &operator=(const Date &d) { 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(11); // 用一个整形变量给日期类型对象赋值 // 实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值 //d1 = 2023;//报错,有explicit关键字隐式转换不了 //d1 = {2023, 11, 6};//同样报错 // 将1屏蔽掉,2放开时则编译失败,因为explicit修饰构造函数,禁止了构造函数类型转换的作用 return 0; }
18、static成员
18.1、static成员概念
-
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数(没有this指针)。静态成员变量一定要在类外进行初始化。
class Date { public: // Date(int year) // : _year(year) {} Date(int year, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {} //static成员函数没有this指针 static void Show(int n) { cout n endl; } private: int _year; int _month; int _day; static int n;//声明 }; int Date::n = 1;//定义 int main() { Date::Show(2); return 0; }
-
面试题:实现一个类,计算程序中创建出了多少个类对象?
class A { public: A() { ++_scount; } A(const A &t) { ++_scount; } ~A() { --_scount; } static int GetACount() { return _scount; } private: static int _scount; }; int A::_scount = 0; int main() { cout A::GetACount() endl; A a1, a2; A a3(a1);//拷贝构造函数 cout A::GetACount() endl; }
18.2、static成员特性
静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
静态成员变量必须在类外定义,定义时不添加
static
关键字,类中只是声明(即静态成员变量必须声明和定义分离)类静态成员函数即可用 类名::静态成员函数名 或者 对象.静态成员函数名 来访问
静态成员函数没有隐藏的
this
指针,不能访问任何非静态成员静态成员也是类的成员,受
public、protected、private
访问限定符的限制class Date { public: // Date(int year) // : _year(year) {} Date(int year, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {} //static成员函数没有this指针 static void Show(int n) { //cout cout n endl; } private: int _year; int _month; int _day; static int n;//声明 }; int Date::n = 1;//定义 int main() { Date::Show(2); Date().Show(2); return 0; }
问题:
静态成员函数可以调用非静态成员函数吗?
答:不可以。因为静态成员函数其实是受限的全局函数(与类有关),调用非静态成员函数需要和类外面调用成员函数一样,使用对象调用,但是在静态成员函数里创建不了对象,所以调用不了非静态成员函数。
非静态成员函数可以调用类的静态成员函数吗?
答:可以。因为静态成员函数其实是受限的全局函数(与类有关),非静态成员函数在函数体里可以通过函数名直接调用静态成员函数,参考在类外面调用静态成员函数只需要加上类限定符
::
即可以访问。
19、友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类
19.1、友元函数
友元函数是指某些不是类成员却能够访问类的所有成员的函数。类授予它的友元特别的访问权。友元函数在类的作用域外定义,但需要在类体中加上关键字friend进行说明。
需要注意的是,友元函数不是类的成员函数,在函数体中访问对象的成员需要使用对象名加运算符”
.
“加对象成员名。同时,友元函数可以访问类中的所有成员,不受public、private、protected
的限制,但其作用域不是该类的作用域。
-
前面我们有使用到这个友元函数,在日期类的实现里面的
和
>>
运算符重载,因为如果要重载这两个运算符,如果重载在类的成员函数里,涉及到一个隐含的this指针作为第一个参数,不符合预期。但是放在类外面重载运算符又访问不了私有的成员变量,所以使用友元函数解决了这个问题。class Date { friend ostream &operator(ostream &_cout, const Date &d); friend istream &operator>>(istream &_cin, Date &d); public: Date(int year = 1900, int month = 1, int day = 1) : _year(year), _month(month), _day(day) {} 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 d; cin >> d; cout d endl; return 0; }
-
注意:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
19.2、友元类
友元类是指一个类可以访问另一个类的私有成员。友元类的概念是为了提高代码的灵活性和可扩展性。具体来说,友元类允许我们将某些类中的私有成员暴露给其他类,而不必公开这些成员。
- 友元关系是单向的,不具有交换性。
- 比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
- 友元关系不能传递。
- 如果C是B的友元, B是A的友元,则不能说明C时A的友元。
- 友元关系不能继承,在继承的地方展开讲。
- 友元类可以在类里面的任何位置声明。
class Time {
friend class 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 = 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;
};
20、内部类
20.1、内部类概念
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类天然就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
class A {
private:
static int k;
int h;
public:
class B // B天生就是A的友元
{
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;
}
20.2、内部类特性
内部类在外部类的public、protected、private地方定义都是可以的。
注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
sizeof(外部类)=外部类,和内部类没有任何关系(内部类相当于外部类的成员函数)。
内部类可以嵌套。
class A {
public:
class B // B天生就是A的友元
{
public:
class C {
public:
void fun_c() {}
class D {//内部类可以嵌套
public:
void fun_d() {
cout "A::B::C::D" endl;
};
private:
int _a;
int _b;
};
private:
int _x;
int _y;
};
void foo(const A &a) {
cout k endl;//OK
cout a._h endl;//OK
}
private:
int _mm;
int _nn;
};
private:
static int k;
int _h;
class E {
public:
void fun_e() {}
private:
int _aa;
int _bb;
};
};
int A::k = 1;
int main() {
A::B b;
b.foo(A());
A::B::C::D abcd;
abcd.fun_d();
cout sizeof(A) endl;
return 0;
}
21、匿名对象
匿名对象是一个没有被命名的对象,通常用于执行一次性操作或者作为中间结果。匿名对象不被分配给任何变量,因此它在使用后会被销毁。
class A {
public:
A(int a = 0)
: _a(a) {
cout "A(int a)" "_a = " _a endl;
}
~A() {
cout "~A()" endl;
}
private:
int _a;
};
class Solution {
public:
int Sum_Solution(int n) {
//...
return n;
}
};
int main() {
A aa1;
//A aa1();// 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
// 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,
// 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
A();//这里就是匿名对象,匿名对象的作用域就在这一行,出了这一行就销毁了
A(2);//匿名对象,参数是2
A aa2(2);
// 匿名对象在这样场景下就很好用,当然还有一些其他使用场景,这个我们以后遇到了再说
Solution().Sum_Solution(10);
return 0;
}
22、构造和拷贝构造时编译器的一些优化
-
有些编译器可能会对同一表达式中,对象在构造和拷贝构造时候进行优化(不同编译器可能不同)。
构造 + 构造 优化为一次构造 (一般不是同一个表达式)
构造 + 拷贝构造 优化为一次构造
拷贝构造 + 拷贝构造 优化为一次拷贝构造
class Date { public: //构造函数 Date(int year, int month = 1, int day = 1) : _year(year), _month(month), _day(day) { cout "Date(int year, int month, int day) " endl; } //拷贝构造函数 Date(Date &d) { cout "Date(Date &d)" endl; } private: int _year; int _month; int _day; }; Date func1(Date d) { Date tmp = d; return tmp; } void func2() { Date d1(1, 2, 3); //构造 + 构造 --> 构造 Date d2 = d1; Date d3 = d2; } int main() { // Date d1(1999, 11, 14); // Date d2(2020,11,14); //同一个表达式中 构造 + 拷贝构造 --> 构造 Date d3 = 1; //同一个表达式中 拷贝构造 + 拷贝构造 --> 拷贝构造 Date d4 = func1(d3); func2(); }
OKOK,C++入门篇3就到这里。如果你对Linux和C++也感兴趣的话,可以看看我的主页哦。下面是我的github主页,里面记录了我的学习代码和leetcode的一些题的题解,有兴趣的可以看看。
Xpccccc的github主页
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net