【引子】初步解读类和对象
类 (class) 和对象 (object) 是两种计算机语言的合称。对象是对客观事物的抽象,而类是对对象的抽象。换句话说,类是现实世界或思维世界中的实体在计算机中的反映,它将数据以及这些数据上的操作封装在一起;而对象是具有类的类型的变量。
类是对象的抽象,而对象是类的具体实例。类是抽象的,在计算机中不占用内存,而对象是具体的,在计算机中占用存储空间。类是用于创建对象的蓝图,它是一个定义包括在特定类型的对象中的方法和变量的软件模板。
什么是类的类型?什么是具有类的类型的变量?为什么类是用于创建对象的蓝图?类和对象是怎么定义的?对象在内存中是如何存储的?这些问题都将在本篇博客中一一解答。
目录
一、类的定义和使用
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
而C++是面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完 成。
C语言结构体中只能定义变量,但在C++中,结构体内不仅可以定义变量,也可以定义函数。例如在往期博客中的通过C语言方式实现栈(【数据结构】线性表之栈和队列),结构体中只能定义变量;但如果以C++方式实现, struct中也可以定义函数。
#include<stdio.h>
//在C语言中,struct中只能定义变量
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int capacity;
int top;
}ST;
#include<iostream>
using namespace std;
//在C++中, struct中不仅能定义变量,还可以定义函数
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;
};
// C++兼容c语言。 struct以前的用法都可以继续用
// 同时struct升级成了类
实际中,对于以上C++中结构体的定义,C++更倾向于用 class 来代替 struct 。
1、类的定义
类的定义分为两个部分,一个是对类名称的定义,一个是对类体(类的成员)的定义。
定义一个类的语法格式为:
class + 类名称 + { + 类体 + } + ;
class className
{
// 类体
}; // 一定要注意 } 后面有一个分号
其中,类体的内容是类的成员,类的成员由成员函数和成员变量组成。
定义类的成员有两种方式,一种是其声明和定义全部放在类体中;另一种是声明和定义分离,例如,类声明放在.h文件中,成员函数定义放在.cpp文件中。在实际项目中,一般更倾向于采取声明和定义分离的方式;而日常练习中,两者均可。
【补】声明和定义的本质区别是,是否开辟空间。声明不开辟空间,定义开辟空间。
ps:成员函数如果在类中定义,编译器可能会将其当成内联函数处理
ps:成员函数名前需要加上“类名称 + :: ”
另外,对于成员变量的定义,建议在变量名前添加前缀(最常用的是下划线),以与函数形参进行区分,避免在使用时(如初始化列表、其他调用成员变量的情景等)混淆。
class Date
{
public:
void Init(int year)
{
year = year; // 这里的year到底是成员变量,还是函数形参?
}
private:
int year;
};
class Date
{
public:
void Init(int year)
{
_year = year; //这样就容易分清成员变量和函数形参了
}
private:
int _year;
};
2、类域
每当定义一个类,也就定义了一个新的作用域,类域。类的所有成员都在类域中,在类体外定义成员时,需要使用作用域操作符“ :: ”,指明成员具体属于哪个类域。
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
//类的成员函数的声明和定义分离
// 这里需要通过“::”指定PrintPersonInfo是属于Person的类域的
void Person::PrintPersonInfo()
{
cout << _name << " "<< _gender << " " << _age << endl;
3、封装和访问限定符
封装是面向对象的三大特性(封装、继承、多态)之一,可以粗浅地理解为将一系列相关的内容“打包”起来。它可以将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
它本质上是一种管理,让用户更方便使用类。例如,对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。对于计算机使用者(用户)而言,不用关心内部核心部件(主板上线路是如何布局的、CPU内部是如何设计的等),只需要知道怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此,计算机厂商在出厂时,在外部套上机箱壳,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以 及键盘插孔等,让用户可以与计算机进行交互即可。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用,选择性地将其接口提供给外部的用户。
类的访问权限有三种:
- 公有权限:公有权限允许外部对象无限制地访问类的所有成员(包括成员变量和成员函数),并且可以通过对象访问。在类的内部和外部,公有成员都可以被访问,也可以在派生类中被继承。
- 私有权限:私有成员被限制为只能在类内部访问。外部对象不能访问任何私有成员,包括成员变量和成员函数。只有在类的成员函数中才可以访问私有变量,私有成员只能被该类的成员函数访问。在派生类中不可访问。
- 保护权限:保护成员可以在类内部和派生类内部访问,外部对象无法访问。与private的不同之处在于,派生类可以访问其基类的保护成员。在类的外部也不能使用保护成员,只能通过类的成员函数来访问(派生类和基类详见继承)。
而控制访问权限,就涉及到了访问限定符。访问限定符有三种,并与访问权限一一对应:public(公有)、private(私有) 和 protected(保护)。但需注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
【Tips】
- public 修饰的成员在类外可以直接被访问;
- protected 和 private 修饰的成员在类外不能直接被访问(此处 protected 和 private 是类似的);
- 访问权限的作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 如果后面没有访问限定符,作用域就到 “ } ”,即,到类结束;
- class 的默认访问权限为 private,而 struct 为 public (这是因为要兼容C)。
【补】C++中struct和class的区别是什么?
C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类,和class定义类是一样的,但区别是struct定义的类的默认访问权限是public,class定义的类的默认访问权限是private(且在继承和模板参数列表位置,struct和class也有区别。在此不多赘述)。
4、类实例化为对象
用类的类型创建对象的过程,称为类的实例化。
一个类可以实例化出多个对象。定义一个类并没有分配实际的内存空间来存储这个类,而是当它实例化出对象时,才分配实际的物理空间存储类成员变量。
class Person
{
public:
//...
private:
char _name[20];
char _gender[3];
int _age;
};
int main()
{
Person._age = 100; // 编译失败:error C2059: 语法错误:“.”
return 0;
}
类是对对象进行描述的,像是一个模型,限定了类有哪些成员。例如:类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间。
5、对象在内存中的大小
类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?该如何计算一个类的大小呢?
// 类中既有成员变量,又有成员函数
class A1 {
public:
void f1(){}
private:
int _a;
};
// 类中仅有成员函数
class A2 {
public:
void f2() {}
};
// 类中什么都没有(空类)
class A3
{};
int main()
{
cout << sizeof(A1) << endl; //4
cout << sizeof(A2) << endl; //1
cout << sizeof(A3) << endl; //1
//[补]
A2 aa1;
A2 aa2;
cout << &aa1 << endl; //000000FE8EEFFCB4
cout << &aa2 << endl; //000000FE8EEFFCD4
return 0;
}
从以上代码不难看出,对象的大小只取决于成员变量,不算成员函数。没有成员变量的类和空类对象比较特殊,需要1byte来占位,以表示对象存在,但不存储有效数据。
对象中只存储的成员变量,没有存储成员函数,而成员函数存放在公共的代码段。
一个对象的大小,实际就是它所对应的类中成员变量的大小之和,且存在内存对齐的情况。
【补】结构体内存对齐规则
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。(VS中默认的对齐数为8)
- 结构体总大小为最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
- 如果结构体嵌套了结构体,则嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
例如:
6、this指针
class Date //日期类
{
public:
void Init(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, d2;
d1.Init(2022,1,11);
d2.Init(2022, 1, 12);
d1.Print();
d2.Print();
return 0;
}
对于以上代码,Date类中有 Init 与 Print 两个成员函数,函数体中没有对不同对象做区分,那当 d1 调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?这是因为C++中引入了this指针。
C++编译器给每个非静态的成员函数增加了一个隐藏的指针参数,该指针指向当前调用该函数的对象。在函数体中所有有关成员变量的操作,都是通过this指针去完成的,只是这个过程对用户是透明的,也就是说,用户不需要操作,编译器会自动完成。
this指针具有以下特性:
- 它只能在成员函数的内部使用;
- this指针本质上是成员函数的形参,当对象调用成员函数时,会将对象地址作为实参传递给this指针形参,所以对象中不存储this指针;
- 因为this指针是形参,所以与普通参数一样,存在函数调用的栈帧里(vs下对this指针传递进行了优化,对象的地址放在ecx,由ecx存储this指针的值);
- this指针是成员函数的第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递;
- this指针的类型为“ 类的类型* const ”,这意味着成员函数不能给this指针赋值。
【补】相关笔试题
//思考:
//1. this指针存在哪里?
//2. this指针可以为空吗?
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
// C
// 语法没有问题故与编译无关;
// p调用print不会发生解引用
// 因为print的地址不在对象中,p会作为实参传给this指针
// 虽然this指针为空,但函数内没有对其解引用,故正常运行
// 2.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
// B
// p调用print不会发生解引用
// 因为print的地址不在对象中,p会作为实参传给this指针
// this指针为空,函数访问_a即是在对其解引用(this->_a),对空指针解引用故运行崩溃
//其他需注意的示例:
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
//不可用
/*A::Print(nullptr);
A::Print(p);*/
return 0;
}
二、类的默认成员函数
一个什么成员都没有的类,被称为空类,然而,空类中并不是真的什么都没有。其实,编译器会为任何类自动生成默认成员函数。
默认成员函数指的是,用户并没有显式实现,而编译器会自动为类生成的成员函数。 它们可以分为构造函数、析构函数、拷贝构造函数和与运算符相关的重载函数。
1、构造函数
构造函数是类的默认成员函数的之一,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
class Date
{
public:
void Init(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, d2;
d1.Init(2022,1,11);
d2.Init(2022, 1, 12);
d1.Print();
d2.Print();
return 0;
}
对于以上代码中的Date类,既可以通过公有的 Init 方法给对象初始化来设置日期,也可以通过构造函数给成员变量初始化。
【ps】虽然名称叫“构造”,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
1)构造函数的特性
它具有以下特性:
- 函数名与类名相同;
- 无返回值;
- 对象实例化时编译器自动调用对应的构造函数;
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义,编译器就将不再生成;
- 构造函数可以重载;
- 内置类型成员变量在类中声明时可以给默认值;
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个(无论是无参构造函数、全缺省构造函数、用户未显式定义而编译器默认生成的构造函数,都可以认为是默认构造函数);
//关于构造函数特性的更多解释
//1
class Date
{
public:
/*
// 如果用户显式定义了构造函数,编译器将不再生成
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类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函
数
// 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再
生成
// 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用
Date d1;
return 0;
}
//2
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;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
// 内置类型成员
// C++11支持,这里不是初始化,而只是声明,所以并没有开辟空间
// 这里给的是默认的缺省值,给编译器生成的默认构造函数用
int _year = 1;
int _month = 1;
int _day = 1;
// 自定义类型成员
//Stack _st;
};
int main()
{
// 构造函数的调用跟普通函数并不相同
//不可以这样调用构造函数
//Date d1;
//d1.Date();
//Date d2;
//d2.Date(2023, 1, 1);
//而要这样调用:类类型 + 对象名称 + “( ” + 初始值 + “ )”
Date d1; //调用无参构造,可以这么写
//Date d1(); //但不可以这样写,会跟函数声明有冲突,编译器不好识别
Date d2(2023, 11, 11); //调用带参构造
d1.Print();
d2.Print();
return 0;
}
//3
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 内置类型
int _year;
int _month;
int _day;
// 自定义类型
Time _t; // 编译器生成的默认构造函数,会对_t调用的它的默认成员函数来初始化
};
int main()
{
Date d;
return 0;
}
//4
class Stack
{
public:
Stack()
{
cout << "Stack()" << endl;
_a = (int*)malloc(sizeof(int) * 4);
if (nullptr == _a)
{
perror("malloc fail");
return;
}
_capacity = 4;
_top = 0;
}
// Stack(int capacity = 4) // 赋缺省值的写法
// {
// cout << "Stack()" << endl;
//
// _a = (int*)malloc(sizeof(int) * capacity);
// if (nullptr == _a)
// {
// perror("malloc申请空间失败");
// return;
// }
//
// _capacity = capacity;
// _top = 0;
// }
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st; //自动调用其专属的构造函数
//Stack st(); //编译器无法区分st()是变量还是函数名
return 0;
}
//内置类型成员不会用构造函数处理;
// (尽管部分编译器会处理,但仍需将不处理当作常态)
// (c++11打了补丁,声明可以给缺省值。给缺省值,编译器会用缺省值初始化,但c++标准没有规定缺省值的初始化,故切不可依赖编译器来初始化)
//自定义类型成员会被调用它专属的默认构造。
//[小结]
//一般情况下,构造函数需要自己写
//(内置类型的成员都有缺省值,且初始化符合我们的实际要求;或者,成员全为自定义类型,且这些成员都定义了默认构造)
【小结】
- 构造函数可以根据有无参数分为无参构造和带参构造;
- 成员变量可以根据其类型分为内置类型成员(语言提供的数据类型)和自定义类型成员(我们使用class/struct/union等自己定义的类型);
- 内置类型成员可以在类中声明时给予默认值来初始化,自定义类型成员初始化会自动调用它“专属”的默认构造函数。
- 一般情况下,类中有内置类型成员,就需要自己写构造函数,不能用编译器自己生成的;如果全部都是自定义类型成员,可以考虑让编译器自己生成,去调用它们“专属”的默认构造;
2)初始化列表
一般来说,对一个类的一个对象进行初始化有两种方式,一种是构造函数体赋值,另一种是初始化列表。
构造函数体赋值在上文中已经出现过。它的原理大致可以概括为,在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date
{
public:
//构造函数体赋值
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
然而,构造函数调用之后(例如以上代码),尽管对象中已经有了一个初始值,但实际上,并不能将这个过程严格地称为“将对象中的成员变量初始化”。构造函数体中的语句可以将其称为赋初值,但不能严格地称作初始化,这是因为初始化实际只能进行一次,而构造函数体内却可以多次赋值。
所以,真正要做到对象的初始化,还是得通过初始化列表。
初始化列表是对象的成员定义的位置。实际书写中,它以冒号开始,紧接着的是以逗号进行分隔的数据成员列表,每个成员变量后紧跟一个放在括号中的初始值或表达式,如以下代码:
class Date
{
public:
//初始化列表
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
};
【ps】
- 每个成员变量至多只能在初始化列表中初始化一次(这是因为初始化本身只能进行一次);
- 引用成员变量、const成员变量、自定义类型成员变量(且其没有构造函数)必须在初始化列表中初始化;
- 成员变量在类中声明的顺序就是其在初始化列表中初始化的顺序,与其在初始化列表中出现的先后顺序无关,为避免隐患,建议类成员变量声明的顺序和定义(在初始化列表中)的顺序保持一致;
- 初始化列表是构造函数的一部分,并不能彻底代替构造函数体赋值;
- 在涉及对象的初始化时,应尽量使用初始化列表,这是因为自定义类型成员变量初始化时的设定,即无论是否使用初始化列表,它们都会优先通过初始化列表来初始化。
//1
class A
{
public:
//A(int a) //不是默认构造函数
// :_a(a)
//{
// cout << "A(int a = 0)" << endl;
//}
A(int a = 0) //_aobj的默认构造函数
:_a(a)
{
cout << "A(int a = 0)" <<endl;
}
private:
int _a; //默认的内置类型成员。它会自动调用它的默认构造函数
};
class B
{
public:
//初始化列表 - 对象的成员定义的位置
B(int a, int ref)
:_ref(ref)
,_n(1)
,_x(2) //当初始化列表中已赋值,不会再用缺省值对内置类型成员进行初始化
//,_aobj(10) //_aobj有默认构造函数时也可如此调用
, _aobj(a) //传参控制初始化.若无默认构造,则编译器要求自己写相应的构造函数调用,否则会报错
{
//_n = 0;
//_ref = ref;
}
private:
//以下都是变量的声明
A _aobj; //此处_aobj没有默认构造函数
//(注:不传参/无参、全缺省、自己不写的才是默认构造函数)
//此时要在初始化列表中赋值,促使它去调用构造函数才可编译通过
//无论有没有默认构造函数,成员变量都要进初始化列表
//可以利用这一点手动传参,以控制参数
//引用成员变量和const成员变量的特征:必须在定义的时候初始化
int& _ref; // 引用成员变量
const int _n; // const成员变量
int _x = 1; // 默认的内置类型成员。这里1是缺省值,缺省值是给初始化列表的。给了缺省值就会用缺省值去初始化
};
int main()
{
//当对象定义了,其整体就定义了
//对象定义就要调用构造函数完成初始化
//而对象的成员在初始化列表中定义(类中)
//B bb1(10, 1);
//B bb2(11, 2);
int n = 10;
B bb1(10, n); //[补] 传引用,可以防止间接造成野指针。但ref是局部变量,出作用域销毁,则原本引用指向的空间也已不存在。故引用并不是绝对安全的
return 0;
}
//2
//一道笔试题:
//以下代码的运行结果为( )
//A.输出1 1
//B.程序崩溃
//C.编译不通过
//D.输出1 随机值
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();
}
// D
//成员变量是按声明的顺序去初始化的,_a2先被初始化,_a1后被初始化,
//_a2初始化时a1尚未初始化,故其值为随机值;而_a1因所传实参而初始化为1
//建议:声明的顺序和定义的顺序保持一致
//3
class Stack //自定义类型Stack
{
public:
Stack(int int capacity = 10)
:_a((int*)malloc(capacity * sizeof(int))) //初始化列表可以用malloc动态空间,但无法检查开辟情况
, _top(0)
, _capacity(capacity)
{
if (nullptr == _a) //故需手动检查malloc
{
perror("malloc fail");
exit(-1);
}
//要求数组初始化
memset(_a, 0, sizeof(int) * capacity); //手动初始化。或直接用calloc
//尽管总有一些工作是初始化列表做不完的
//但无论是否使用初始化列表,自定义类型成员都会优先使用初始化列表进行初始化
//故建议直接使用初始化列表
}
private:
int* _a;
int _top;
int _capacity;
};
//4
//开辟二维动态数组的情景,无法使用初始化列表进行初始化,必须通过函数体赋值的方式
class AA
{
public:
AA(int row=10, int col=5)
:_row(row)
,_col(col)
{
//开辟二维动态数组
int** _aa = (int**)malloc(sizeof(int*) * row); //开辟一位的指针数组
for (int i = 0; i < row; i++)
{
_aa[i] = (int*)malloc(sizeof(int) * col);
}
}
private:
int* _a;
int _row;
int _col;
};
//故按需初始化即可
3)explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。explicit关键字的作用,是阻止类型转换的发生。
//1.隐式类型转换
class A
{
public:
A(int a)
:_a(a)
{
cout << "A(int a)" << endl; //代码运行后,构造函数会被调用
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl; //但拷贝构造不会被调用
}
private:
int _a;
};
int main()
{
int i = 10;
double d = i; //类型转换会产生目标类型的临时变量,再将临时变量的值赋给目标变量
A aa1(1); //调用构造
A aa2 = 2; //隐式类型转换 (一个整型转换为自定义类型)
//用2调用A类型的构造函数,生成一个A类型的临时变量,再用这个临时变量进行拷贝构造
//按语法原义即:构造+拷贝构造
//但编译器会优化,优化为2直接构造aa2。编译器不允许在同一个表达式中连续的构造,不允许构造一个变量再去拷贝构造
//在连续的表达式里,编译器一般都会优化,以此提高效率
//A& aa3 = 2; //无法从int转换为A&,不可通过编译
cosnt A& aa3 = 2; //编译通过,且调用了一次构造函数
//用2构造一个临时变量,临时变量具有常性,不可引用,但可以const引用
return 0;
}
#include<string>
#include<list>
class string //string类
{
public:
string(const char*str)
{}
};
int main()
{
string name1("zhangsan"); //构造
string name2="zhangsan"; //构造+拷贝构造+编译器优化
//虽然两者都是构造,且结果相同,但两者过程不同,意义不同
return 0;
}
//而这些特性存在的意义在于:
class list //链表
{
public:
void push_back(const string&str)
{}
};
int main()
{
list lt1;
//写法1
string name3("lisi");
lt1.push_back(name3);
//写法2
lt1.push_back("lisi"); //lisi可以传入push_back是因为隐式类型转换
//以上两种写法效果相同,但写法2更加精简
}
// 2.explicit关键字的用法
//如果不想让隐式类型转换发生,可以在构造前加入explicit关键字
class A
{
public:
explicit A(int a)
:_a(a)
{
cout << "A(int a)" << endl; //代码运行后,构造函数会被调用
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl; //但拷贝构造不会被调用
}
private:
int _a;
};
int main()
{
A aa1(1);
A aa2 = 2; //加入explicit关键字编译不再通过,无法从int转换为A类型
}
//explicit的具体使用与智能指针有关
2、析构函数
“一个对象是怎么来的?”跟构造函数有关,而“一个对象是怎么没的?”跟析构函数有关。
析构函数也是是类的默认成员函数的之一,功能与构造函数相反。对象在销毁时会自动调用析构函数,完成对象资源的清理工作。
【ps】析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。
它具有以下特性:
- 函数名是在类名前加上字符“ ~ ”;
- 无参数也无返回值类型;
- 对象生命周期结束时,编译器会自动为对象调用析构函数;
- 一个类只能有一个析构函数,若未显式定义,编译器会自动生成默认的析构函数;
- 不能重载;
- 编译器生成的默认析构函数,会对自定义类型成员调用它“专属”的析构函数;
- 如果类中没有动态的内存申请时,析构函数可以不写,直接使用编译器生成的默认析构函数;有动态的内存申请时,一定要写,否则会造成内存泄漏;
- 当多个对象调用了其构造函数,它们后续调用其析构函数的顺序与之相反。
//关于析构函数特性的更多解释
//1
class Stack
{
public:
Stack(int capacity = 4)
{
cout << "Stack()" << endl;
_a = (int*)malloc(sizeof(int) * capacity);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = capacity;
_top = 0;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
private:
int* _a = nullptr;
int _top = 0;
int _capacity;
};
int main()
{
Stack st;
return 0;
}
//出作用域后析构函数被自动调用
//同样的,内置类型不作处理,自定义类型会调用它的析构函数
//2
// 不开辟动态空间无需写析构,因为调用结束后空间立刻销毁,例如以下日期类Date
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;
};
//3
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
// 为什么程序运行结束后输出:~Time()?
// 在main函数中根本没有直接创建Time类的对象,但最后会调用Time类的析构函数,
// 这是因为main函数中创建了Date对象d,而d中包含4个成员变量,其中_year, _month,
// _day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;
// 而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构数。
// 但是main函数中不能直接调用Time类的析构函数,因为实际要释放的是Date类对象,所以编译器会调用Date类的析构函数。
// 而Date没有显式提供,于是编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,
// 即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁。
// main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数
// [小结]创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数
3、拷贝构造函数
拷贝构造函数是构造函数的一个重载形式,在创建对象时,可以利用一个已存在对象来创建一个一模一样的新对象。它只有单个形参,这个形参是对其所在的类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时,由编译器自动调用。
它具有以下特性:
- 它是构造函数的一个重载形式;
- 它的参数只有一个且必须是类类型对象的引用,使用传值方式编译器会直接报错(会引发无穷递归);
- 它的典型调用场景有: 使用已存在对象创建新对象、函数参数类型为类类型对象、函数返回值类型为类类型对象;
- 若未显式定义,则编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝;内置类型成员是按照字节方式直接拷贝的(这种拷贝叫做浅拷贝,或者值拷贝,特点是不额外开辟空间),而自定义类型成员是调用其专属的拷贝构造完成拷贝的;
- 如果类中没有涉及动态的内存申请时,拷贝构造可写可不写;一旦涉及动态的内存申请时,拷贝构造就是一定要写的,否则就是浅拷贝。
//关于拷贝构造特性的更多解释:
//1
class Date
{
public:
//构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造
// d2(d1)
//Date(Date& d) //拷贝构造有且只有一个参数,且必须是类的类型对象的引用
Date(const Date& d) //最好加上一个const引用,以防拷贝源和拷贝目标混淆
{
cout << "Date(Date& d)" << endl;
/*this->*/_year = d._year;
_month = d._month;
_day = d._day;
//错误写法
/*d._year = _year;
d._month = _month;
d._day = _day;*/
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 4, 25);
Date d2(d1); //d是d1的别名传给拷贝构造函数,d2传给this指针
return 0;
}
//2
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// Date(const Date& date) // 正确写法
Date(const Date date) // 错误写法:编译报错,会引发无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1; //调用构造创建d1
Date d2(d1); //调用拷贝构造利用d1创建d2
return 0;
}
//3
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time(const Time& t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
cout << "Time::Time(const Time&)" << endl;
}
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);
// 用已经存在的d1拷贝构造出d2,此处会调用Date类的拷贝构造函数
// 但Date类并没有显式定义拷贝构造函数,于是编译器会给Date类生成一个默认的拷贝构
造函数
return 0;
}
//在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,
//而自定义类型是调用其拷贝构造函数完成拷贝的。
//若未显示定义,编译器会生成默认的拷贝构造函数
//内置类型成员完成浅拷贝;自定义类型成员会调用它专属的拷贝构造
//也就是说,就算没有写拷贝构造函数,内置类型和自定义类型也都会被编译器处理,但是以浅拷贝的方式
//浅拷贝(不单独开辟空间)的问题:1.若发生析构两次则会报错,2.其中拷贝的一个被修改会影响另一个
//4
//下面的程序会崩溃掉,因为默认的拷贝构造是以浅拷贝的方式实现的
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)
{
_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;
}
//栈的拷贝实际是深拷贝(特点是开辟新空间),深拷贝必须由我们自己实现
//如果类中没有涉及动态的内存申请时,拷贝构造可写可不写;
//一旦涉及动态的内存申请时,拷贝构造就是一定要写的,否则就是浅拷贝
//5(接4)
//深拷贝实现
class Stack
{
public:
Stack(int capacity = 4)
{
cout << "Stack()" << endl;
_a = (int*)malloc(sizeof(int) * capacity);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = capacity;
_top = 0;
}
// st2(st1)
Stack(const Stack& st)
{
_a = (int*)malloc(sizeof(int) * st._capacity);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
memcpy(_a, st._a, sizeof(int) * st._top);
_top = st._top;
_capacity = st._capacity;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
//6
//构造、析构、拷贝构造调用顺序一览
class Date
{
public:
Date(int year, int minute, int day)
{
cout << "Date(int,int,int):" << this << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d):" << this << endl;
}
~Date()
{
cout << "~Date():" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
Date Test(Date d)
{
Date temp(d);
return temp;
}
int main()
{
Date d1(2022,1,13);
Test(d1);
return 0;
}
//为了提高程序效率,一般在对象传参时,尽量使用引用类型,
//而返回时,根据实际场景能用引用也尽量使用引用。
4、运算符重载
运算符重载函数是具有特殊函数名的函数,也具有返回值类型、函数名字和参数列表,其返回值类型、参数列表与普通的函数类似。
它与关键字“ operator ”有关。定义一个运算符重载函数的语法格式为:
返回类型 + operator + 要重载的运算符 + 参数列表
它具有以下特性:
- 不能通过连接其他符号来创建新的运算符,例如:operator@;
- 重载操作符必须有一个类类型参数;
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型“ + ”,不能改变其含义;
- 它作为类的成员函数时,其形参数量看上去比操作数的数量少1,其实这是因为成员函数的第一个参数为隐藏的this;
- “ * ”、“ :: ”、“ sizeof ”、“ ? :(三目运算符)”、“ .(成员访问)”,这五个运算符不能重载。
//关于运算符重载特性的更多解释:
//1
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;
};
// 全局的operator==
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;
}
// 将运算符重载为全局函数,就需要成员变量非私有而是公有的,否则无法访问类的成员变量
// 但如此一来,封装性又如何保证呢?
// 这里有两种方式,一种是通过友元(见下文),另一种是将其重载为类的成员函数。
//2
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)
bool operator==(const Date& d2)
{
return _year == d2._year;
&& _month == d2._month
&& _day == d2._day;
}
// 这里需要注意的是,左操作数是this,指向调用函数的对象
private:
int _year;
int _month;
int _day;
};
1)赋值重载
- 当赋值运算符重载函数没有显示定义或声明时,编译器会自动生成一个默认的赋值运算符重载,以值的方式逐字节拷贝。
- 默认生成的赋值重载跟拷贝构造的行为一样,内置类型成员进行浅拷贝,自定义类型成员会去调用它专属的赋值重载。
- 如果类中未涉及到动态的内存申请(深拷贝),赋值重载可写可不,而一旦涉及到动态的内存申请,就一定要自己写。
//1
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) //传引用返回不调用拷贝函数,避免占用空间资源,大大增加效率
{
if (this != &d) //断言防止自己赋值自己,例如:d1 = d1。地址相同则不赋值
//if (*this != d) //此处如果比较值,还要调用赋值函数,代价太大,不如比地址
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this; //this是形参,但其生命周期不仅仅在类的成员函数中
}
private:
int _year;
int _month;
int _day;
};
//ps:
//以对象作为函数参数时,或当传入的形参为引用时,不会调用拷贝构造;
//(函数)返回类对象时,或当返回值为引用时,也不会调用拷贝构造。
//避免自己赋值自己,是因为:
//1、“自己赋值自己”本身没有实际意义。
//2、如果类的成员变量中含有指针,自己赋值自己可能会带来巨大隐患。
// 指针间的赋值,要先将作为拷贝目标的指针所指向的空间释放掉,然后再为它重新分配空间,
// 然后将作为拷贝源的指针的所指内容,拷贝到作为拷贝目标的指针当前所指的空间。
// 如果是自己赋值自己,作为拷贝目标的指针和作为拷贝源的指针就是同一个,
// 在赋值操作前对作为拷贝目标的指针的释放空间,同时也会使其所指的数据被销毁。
//2
//ps:赋值重载只能重载成类的成员函数不能重载成全局函数
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 =”必须是非静态成员
//赋值重载如果不显式实现,编译器就会生成一个默认的赋值重载。
//此时再在类外实现一个全局的赋值重载,就会和编译器在类中生成的默认赋值重载发生冲突,
//故赋值运算符重载只能是类的成员函数。
//3
//用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝
//内置类型成员进行浅拷贝,自定义类型成员会去调用它专属的赋值重载
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;
//[补]
Date d4 = d2; // 用一个已经存在的对象初始化另一个对象;此处是拷贝构造,等价于 Date d4(d2);
//拷贝+初始化
d4 = d1; //已经存在的两个对象之间复制拷贝;此处是赋值,调用运算符重载函数
//赋值是单纯的拷贝
return 0;
}
2)取地址和const取地址重载
编译器会默认生成取地址重载和const取地址重载,故这两个默认成员函数一般不用自己写 ,直接使用编译器生成的重载函数即可。唯独特殊情况,例如限定他人的访问内容,才需要自己来写。取地址重载针对的是普通对象取地址的情景,const取地址重载针对的是const对象取地址的情景。
class Date
{
public:
Date* operator&() //适应普通对象
{
//cout << "Date* operator&()" << endl;
return this ;
}
/*Date* operator&()
{
return nullptr; //这样写,就使普通对象不能取成员的地址,唯独const对象可以取成员地址
}*/
const Date* operator&()const //适应const对象
{
//cout << "const Date* operator&() const" << endl;
return this ;
}
private:
int _year ;
int _month ;
int _day ;
};
三、特别的类成员
1、const(常量)成员
(本节主要说的是被const修饰的成员函数)
被const修饰的成员函数被称为常函数。它的语法格式为:
返回类型 + 函数名 + “( ” + 参数列表 +“ )” + const
const修饰类成员函数,实际修饰的是该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。成员函数后加const,普通对象和const对象都可以调用,但不是所有成员函数后都能加const,例如修改成员变量的函数。
【Tips】 只要成员函数内部不涉及修改成员变量,其后都应该加上const,这样一来,普通对象和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;
}
//“ + ”重载
Date operator+(int day) const //出了作用域tmp销毁,故不用引用返回
{
Date tmp(*this); //拷贝构造一个tmp,不改变d1
tmp._day += day;
while (tmp._day > GetMonthDay(tmp._year, tmp._month))
{
tmp._day -= GetMonthDay(tmp._year, tmp._month);
++tmp._month;
if (tmp._month == 13)
{
++tmp._year;
tmp._month = 1;
}
}
return tmp;
}
//“ < ”重载
bool operator<(const Date& x) const
{
if (_year < x._year)
{
return true;
}
else if (_year == x._year && _month < x._month)
{
return true;
}
else if (_year == x._year && _month == x._month && _day < x._day)
{
return true;
}
return false;
}
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
Date d1(2023, 5, 5);
d1.Print(); //转化成d1.Print(&d1);
//传给void Print(): Date*this,权限平移
const Date d2(2023, 5, 5);
d2.Print(); //无法调用Print()
//转化成d2.Print(&d2);
//传给void Print(): const Date*this,权限放大
//将this指针改成 const Date* this 即可
//而 d1.Print(&d1) 传给此时的Print()相当于是权限缩小
//void Print() const - 普通对象和const对象都可以调用
//成员函数后加const,普通对象和const对象都可以调用
//但不是所有成员函数后都能加const,例如修改成员变量的函数就不能加
d1 + 100;
d2 + 100; //const变量不能调用operator+(int day),
//必须在operator+(int day)后加上const
//operator+(int day)不改变成员变量,故可加const
d1 < d2;
d2 < d1;
//d1和d2可比较大小
//但d2和d1不可比较大小
//故在operator<(const Date& x)后加const,原理同上
//[小结]
//只要成员函数内部不修改成员变量,其后都应该加上const
//这样一来,普通对象和const对象都可以调用相应函数,不涉及权限问题
}
int main()
{
TestDate();
return 0;
}
2、static(静态)成员
声明为static修饰的类成员称为类的静态成员。用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,则称之为静态成员函数。
静态成员具有以下特性:
- 静态成员存放在静态区,为所有类对象所共享,不属于任何一个具体的对象;
- 静态成员也是类的成员,受 public、protected、private 访问限定符的限制;
- 静态成员可用“ 类名 :: 静态成员 ”或者“ 对象.静态成员 ”来访问;
- 静态成员变量必须在类外定义,定义时并不添加static,类中只是声明,声明时需要添加static;
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员。
//1
//场景实例:实现一个类,计算程序中创建出了多少个类对象
//如下,统计类A的构造被调用多少次,以便知道创建了多少个对象
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;
void TestA()
{
cout << A::GetACount() << endl; //0
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl; //3
}
int main()
{
TestA();
}
//2
//续1
int _scount = 0;
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
private:
};
A aa0;
void Func()
{
static A aa2;
cout << __LINE__ /*当前这行代码对应行数*/<< ":" << _scount << endl;
//_scount++; //但这个代码有一个缺陷。全局变量的劣势:可以在任何地方随意改变
}
int main()
{
cout << __LINE__ << ":" << _scount << endl; // 1
A aa1;
Func(); // 3
Func(); // 3
return 0;
}
//全局变量的劣势:可以在任何地方随意改变
//为了解决这个问题,可以把静态变量作为静态成员变量封装到类中
//3
//续2
//全局变量的劣势:可以在任何地方随意改变
//为了解决这个问题,可以把静态变量作为静态成员变量封装到类中
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
void Func1()
{
// 非静态可以调用静态
GetACount();
}
void Func2()
{
++_a1;
}
static int GetACount()
{
//_a1++; //静态不可用调用非静态。非静态的成员函数调用需要this指针,静态成员函数没有this
//Func2() //静态不可用调用非静态
return _scount;
}
private:
// 成员变量 -- 属于每一个类对象,存储在对象里面
int _a1 = 1;
int _a2 = 2;
// 静态成员变量 -- 属于类,类的每个对象会共享,存储在静态区
static int _scount; //必须在类外(全局位置)定义。静态成员不能给缺省值(缺省值是传给初始化列表的)
};
int main()
{
cout <<__LINE__<<":"<< A::GetACount() << endl; //0
A a1;
A a2(a1);
cout << __LINE__ << ":" << A::GetACount() << endl; //2
return 0;
}
//可以通过公有的静态成员函数访问私有的静态成员变量
//静态成员函数的特点:没有this指针,指定类域和访问限定符就可以访问
//但静态不可用调用非静态。非静态的成员函数调用需要this指针,静态成员函数没有this
【补】相关笔试题
//题目:
//求1 + 2 + 3 + ... + n,
//要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)
class Sum
{
public:
Sum()
{
_ret += _i;
++_i;
}
static int GetRet()
{
return _ret;
}
private:
static int _i;
static int _ret;
};
int Sum::_i = 1;
int Sum::_ret = 0;
class Solution
{
public:
int Sum_Solution(int n)
{
Sum a[n]; //n个Sum对象会调用n次构造函数
return Sum::GetRet(); //用静态成员变量来计数
}
};
//题目:
//设计一个类,在类外面只能在栈上或堆上创建对象
class A
{
public:
static A GetStackObj()
{
A aa;
return aa;
}
static A* GetHeapObj()
{
return new A;
}
private:
A() //构造函数私有化
{}
private:
int _a1 = 1;
int _a2 = 2;
};
int main()
{
//static A aa1; //静态区
//A aa2; //栈
//A* ptr = new A; //堆
A::GetStackObj();
A::GetHeapObj();
return 0;
}
四、友元
友元提供了一种突破封装的方式,提供了便利,但总得来说,友元会增加耦合度而破坏封装,所以不宜多用。 它分为友元函数和友元类,涉及关键字“ friend ”。
1、友元函数
友元函数可以化解成员函数隐含的this指针参数与实际所需的参数发生冲突的问题。
先来看如下例子:
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
// 这是因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
ostream& operator<<(ostream& _cout)
{
_cout << _year << "-" << _month << "-" << _day << endl;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
//尝试在类中重载“<<”,却发现无法将operator<<重载成成员函数,
//这是因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。
//this指针默认是第一个参数(也就是左操作数),
//但cout需要的是第一个形参对象,
//所以要将“<<”重载成全局函数,
//但这样又会导致类外没办法访问成员。
//此时可以运用友元来解决。
class Date
{
friend ostream& operator<<(ostream& _cout, const 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;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
友元函数的特性:
- 它可访问类的私有和保护成员,但它不是类的成员函数;
- 它不能用const修饰;
- 它可以在类中任何地方声明,不受类访问限定符限制;
- 一个函数可以是多个类的友元函数;
- 它的调用与普通函数的调用原理相同;
2、友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
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;
};
友元类的特性:
- 友元关系是单向的,不具有交换性(比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行);
- 友元关系不能传递 (如果C是B的友元, B是A的友元,但不能说明C时A的友元);
- 友元关系不能继承。
五、内部类
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类, 它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类并没有任何优越的访问权限。内部类就相当于是外部类的友元类,可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
class A
{
private:
static int k;
int h; //h的大小是必须算的
//public:
class B //内部类其实是外部类的“天生”友元
{
public:
void foo(const A& a) //因为是友元,所以可以在B类中访问A类对象的成员
{
cout<<k<<endl;
cout<<a.h<<endl;
}
private:
int b; //B声明在A的类域中,是A的内部类,不占用空间(因为只是声明)
};
};
int A::k = 1; //k没有存入对象中,故无须计算k的大小
int main()
{
cout << sizeof(A) << endl; //A类的大小是4
A aa; //创建一个A类对象,并不会受到B类的限制
//B bb; //不可单独创建B类的对象
A::B bb; //必须从A中指定B类来创建对象,会受到A类的限制
return 0;
}
内部类的特性:
- 内部类可以定义在外部类的public、protected、private都是可以的;
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象或类名;
- sizeof(外部类)=外部类的大小,和内部类没有任何关系。
【补】相关笔试题
//题目:
//求1 + 2 + 3 + ... + n,
//要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A ? B : C)
//内部类写法:
class Solution
{
class Sum
{
public:
Sum()
{
_ret +=_i;
++_i;
}
};
public:
int Sum_Solution(int n)
{
Sum a[n];
return _ret;
}
private:
static int _i; //内部类不可访问外部类的成员
static int _ret; //故将外部类成员改为静态成员,使内部类和外部类均可访问
};
int Solution::_i = 1;
int Solution::_ret = 0;
六、匿名对象
匿名对象就是在定义之初无名的类类型对象,且具有常性。
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl; //检验是否调用构造
}
~A()
{
cout << "~A()" << endl; //检验是否调用析构
}
private:
int _a;
};
class Solution {
public:
int Sum_Solution(int n) {
cout << "Sum_Solution" << endl; //检验是否调用成员函数
return n;
}
};
int main()
{
A aa(1); //有名对象 - 生命周期在当前函数的局部域
A(2); //匿名对象 - 生命周期仅在当前行。和普通有名对象一样,会传参调用构造
//调用成员函数
//法1:有名对象调用
Solution sl;
sl.Sum_Solution(10);
//法2:匿名对象调用
Solution().Sum_Solution(20); //除了调用成员函数,该行代码还会调用一次析构
//因为生命周期仅在当前行,故匿名对象即用即销毁
//区分 Solution sl; 和 Solution().Sum_Solution(20);
//1.Solution().Sum_Solution(20)的Solution后加(),是因为调用普通成员函数要传this指针
//2.Solution().Sum_Solution(20)不能写成 Solution::Sum_Solution(20) ,是因为Solution::Sum_Solution(20)是静态成员函数的写法,
// 而静态成员函数无须传this指针,故该写法不传this指针,不符合普通成员函数的语法规范
//3.Solution sl 后不能加(),是因为加入后编译器无法区分是对象还是函数名
//A& ra = A(1); //匿名对象和普通有名对象一样,也具有常性
const A& ra = A(1); //const引用会延长匿名对象的生命周期为当前函数局部域
return 0;
}
//[补] const引用会延长匿名对象的生命周期为当前函数局部域,实例:
//#include<string>
void push_back(const string& s)
{
cout << "push_back:" << s << endl;
}
int main()
{
string str("11111");
push_back(str);
push_back(string("222222"));
push_back("222222");
return 0;
}
七、拷贝优化
拷贝对象时,一些编译器会作优化处理。在传参和传返回值的过程中,编译器会通过一些优化方式——同一行的同一个表达式中,连续构造和拷贝构造会合二为一,优化成一次构造——减少对象的拷贝。这在某些特定场景下是非常有用的。
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a = aa._a;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
void Func1(A aa)
{
}
//void Func1(const A& aa) //与上一个函数构成重载,但调用不明确
//{
//
//}
void Func2(const A& aa)
{
}
A Func3()
{
A aa;
return aa;
}
A& Func4()
{
static A aa;
return aa;
}
A Func5()
{
A aa;
return aa;
}
int main()
{
A a1;
Func1(a1); //有拷贝构造
Func2(a1); //无拷贝构造
Func3(); //拷贝作为函数的返回值
Func4(); //没有拷贝构造
//用引用返回,可以减少拷贝,提高效率
//A& ra = Func5(); //返回值是A拷贝的临时变量,不能这样接收返回值,因为非常量引用(非const引用)只能绑定到左值
const A& ra = Func5();
A ra = Func5(); //Func5中拷贝一次,等于赋值拷贝一次,故一共拷贝两次
Func5(); //这种写法拷贝次数同上
//编译器优化:同一行的同一个表达式中,连续构造+拷贝构造会合二为一,优化成一次构造
A aa1;
Func1(aa1); // 不连续的两行表达式,不会优化
Func1(A(1)); // 连续的一行表达式,会优化
A aa2 = 1; // 构造+拷贝构造 => 优化为构造
Func1(1); // 构造+拷贝构造 => 优化为构造(与上行其实等价,只不过涉及隐式类型转换)
//写法1
A ra1 = Func5(); //同理,会优化
//写法2
A ra2;
ra2 = Func5(); //对象已定义,此处是赋值操作,不会优化
//第一种种写法会分别调用一次构造、一次拷贝构造、一次析构
//但第二种写法会调用两次构造、一次拷贝构造、一次赋值、四次析构,代价较大
//注:尽管编译器的优化方案不同,但都会往相似的方向进行优化。故推荐第一种写法,效率更高。
return 0;
}
【总结】重新解读类和对象
类是对象的抽象,而对象是类的具体实例,它们属于一种基于“面向对象”思想诞生的语言。面向对象的语言诞生的意义在于,让计算机能够正确理解人的意图。
计算机并不能理解现实生活中的实体,计算机只理解二进制格式的数据。如果想要让计算机理解现 实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序、创建对象让计算机理解。
例如,想要让计算机理解“洗衣机”,就需要:
- 用户先要对现实中洗衣机实体进行抽象,即在人为思想层面对洗衣机进行认识,洗衣机有什 么属性,有那些功能,即对洗衣机进行抽象认知的一个过程;
- 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清 楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、Java、Python等)将洗衣机用类来进行描述,并输入到计算机中;
- 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣 机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才 能洗衣机是什么东西;
- 此时,用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。
类是对某一类实体(对象)来进行描述的,描述该对象具有哪些属性、哪些方法,描述完成后,就形成了一种新的自定义类型,通过该自定义类型就可以实例化具体的对象。
【补】手动实现一个日期类
Date.h(类的定义、成员变量的定义、成员函数的声明等)
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<assert.h>
using namespace std;
class Date
{
//友元函数
//通过友元函数的声明,让全局函数可以访问私有
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1, int month = 1, int day = 1);
void Print() const //此处的const修饰的是*this,因为this指针不能显示地写
{
cout << _year << "-" << _month << "-" << _day << endl;
}
bool operator<(const Date& x) const;
bool operator==(const Date& x)const;
bool operator<=(const Date& x)const;
bool operator>(const Date& x)const;
bool operator>=(const Date& x)const;
bool operator!=(const Date& x)const;
//int GetMonthDay(int year, int month); //内置类型传值即可,不必传引用
static int GetMonthDay(int year, int month);
// d1 + 100
Date& operator+=(int day);
Date operator+(int day) const;
//两个运算符重载可同时存在,相当于两个但功能不同同名的函数重载
//但需要加一个参数来区别两者
Date& operator++();
Date operator++(int);
Date& operator-=(int day);
Date operator-(int day) const;
Date& operator--();
Date operator--(int);
int operator-(const Date& d)const; //计算日期差
//流插入
//这是一个运算符重载,两个运算符重载构成了函数重载,
//可以支持内置类型(因为库ostream里面已经实现了),
//也可以支持自定义识别类型(因为函数重载)
//但流插入不推荐写成成员函数,因为Date对象默认占用第一个参数就做了左操作数
//写出来注定是下面这个样子,可以运行但不符合使用习惯
//d1 << cout;
//除非让cout做第一个操作数(写到全局即可实现)
//void operator<<(ostream& out);
//int GetYear() //可以通过成员函数让全局函数operator<<访问私有,但无法修改私有
//{
// return _year;
//}
//故通过友元函数实现
Date* operator&()
{
//cout << "Date* operator&()" << endl;
//return this;
return nullptr; //使普通对象不能取成员地址,const对象可以取成员地址
}
const Date* operator&() const
{
//cout << "const Date* operator&() const" << endl;
return this;
}
private:
/*int _year;
int _month;
int _day;*/
//赋缺省值,让const Date d2初始化
int _year=1;
int _month=1;
int _day=1;
};
//全局函数的流插入,但无法直接访问私有成员
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
Date.cpp(成员函数的定义等)
#include"Date.h"
Date::Date(int year, int month, int day)
{
if (month>0&&month<13
&&day>0&&day<=GetMonthDay(year,month))
{
_year = year;
_month = month;
_day = day;
}
else
{
cout << "非法日期" << endl;
assert(false);
}
}
bool Date::operator<(const Date& x) const
{
if (_year < x._year)
{
return true;
}
else if (_year == x._year && _month < x._month)
{
return true;
}
else if (_year == x._year && _month == x._month && _day < x._day)
{
return true;
}
return false;
}
bool Date::operator==(const Date& x)const
{
return _year == x._year
&& _month == x._month
&& _day == x._day;
}
bool Date::operator<=(const Date& x)const
{
return *this < x || *this == x; //复用
}
bool Date::operator>(const Date& x)const
{
return !(*this <= x);
}
bool Date::operator>=(const Date& x)const
{
return !(*this < x); //复用
}
bool Date::operator!=(const Date& x)const
{
return !(*this == x);
}
int Date::GetMonthDay(int year, int month)
{
static int daysArr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 }; //将数组放到静态区,不用每次调用函数都创建一次数组,优化效率
//if (((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) && month == 2)
if (month==2&&((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
return 29;
}
else
{
return daysArr[month];
}
}
Date& Date::operator+=(int day)
{
if (day < 0)
{
return *this -= -day;
}
//持续进位,天满了往月进位,月满了往年进位
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;
/** this = *this + day; //此法要创建4个对象
return *this;*/
//Date tmp(*this); //此法只创建2个对象
//tmp += day;
//return tmp;
}
Date Date::operator+(int day) const //出了作用域tmp销毁,故不用引用返回
{
Date tmp(*this); //拷贝构造一个tmp,不改变d1
/*tmp._day += day;
while (tmp._day > GetMonthDay(tmp._year, tmp._month))
{
tmp._day -= GetMonthDay(tmp._year, tmp._month);
++tmp._month;
if (tmp._month == 13)
{
++tmp._year;
tmp._month = 1;
}
}
return tmp;
*/
tmp += day; //复用
return tmp;
}
//前置++
Date& Date::operator++()
{
*this += 1;
return *this;
}
//后置++
//增加这个int参数不是为了接收具体的值,仅仅是占位,跟前置++构成重载,方便区分
Date Date::operator++(int)
{
Date tmp = *this;
*this += 1;
return tmp;
}
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) const
{
Date tmp = *this;
tmp -= day;
return tmp;
}
Date& Date::operator--()
{
*this -= 1;
return*this; //无对象拷贝
}
Date Date::operator--(int)
{
Date tmp = *this; //需拷贝一次对象
*this -= 1;
return tmp;
}
//前置无须拷贝,节省空间,效率更高,故前置更好
//计算日期差
int Date::operator-(const Date& d)const
{
//法1:
//先把月和天数对齐,再算年份
//eg:2023-5-5 - 2000-10-9 = (2023-5-5 - 2023-10-9) + (2023-10-9 - 2000-10-9)
//法2:
//先判断两个日期谁大谁小,让小的去不断加天数,直至与大的日期相等
Date max = *this;
Date min = d;
int flag = 1; //默认第一个大第二个小
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0; //计数天数
while (min != max)
{
++min;
++n;
}
return n * flag;
}
//做成员函数的流插入
//void Date::operator<<(ostream& out)
//{
// out << _year << "年" << _month << "月" << _day << "日" << endl;
//}
//全局函数的流插入
ostream& operator<<(ostream& out, const Date& d) //加const防止改变日期类,但该函数要往out中插入,故ostream不能加const
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
//流提取
istream& operator>>(istream& in, Date& d) //流要读要写,不能加const,Date& d 同理
{
//in >> d._year >> d._month >> d._day;
//if (d._month > 0 && d._month < 13
// && d._day>0 && d._day <= d.GetMonthDay(d._year, d._month))
//{
// return in;
//}
//else
//{
// cout << "非法日期" << endl;
// assert(false);
//}
int year, month, day;
in >> year >> month >> day;
if (month > 0 && month < 13
&& day > 0 && day <= d.GetMonthDay(year, month))
{
d._year = year;
d._month = month;
d._day = day;
}
else
{
cout << "非法日期" << endl;
assert(false);
}
return in;
}