【C++】简单学——类和对象(下)
初始化列表
前提:
对象实例化,成员变量就整体定义了,那么成员变量是在哪里单体定义初始化的?构造函数处吗?
概念
概念:初始化列表是每个的成员定义初始化的位置
位置:在构造函数底下
结构: :代表开始 ,代表分点
class Date
{
public:
// // 初始化列表
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
}
语法
- 一个成员变量在初始化列表中只能出现一次
- const成员变量、引用、没有默认构造函数的自定义类型,这三个必须走初始化列表(因为这三个必须要在定义的时候初始化)
特性
- 这是所有成员变量定义初始化的地方,你只要定义,就必须走初始化列表(不写就是给随机值),就算你在初始化列表里面不显示走成员变量,他们也会走初始化列表
- 自定义类型的成员不写在初始化列表里也会走初始化列表,然后调用它的默认构造(如果没有默认构造就只能自己在初始化列表传参来进行调用;如果不手动调用又没有默认构造,就会报错)
- 初始化列表的初始化顺序是声明从上到下的顺序,不看列表自己的顺序(如果成员的值依靠其他成员,就要小心初始化顺序)
为什么要有这个
- 在默认构造函数体内初始化时,函数体里面没办法对具体操作加以规范,函数体里面的操作什么样都可以,成员变量多次赋值也管不着,不够规范
- 构造函数没办法解决三种成员变量:某些需要在定义的时候初始化的值必须要走初始化列表(const成员,引用成员,没有默认构造函数的自定义类型成员)
声明处的给缺省值
C++11支持在成员变量声明的地方给缺省值
这个值给的就是初始化列表中括号的位置(没给才会用缺省值)
private:
int a = 1;
int* p1 = nullptr;
int* p2 = (int*)malloc(4);这个地方就是函数参数列表那里的缺省值int * _p = (int *)malloc(4);
Date d1 = 3;
Date d2 = {1, 2, 3};
分工建议(和构造函数)
- 初始化列表进行定义和初始化
- 构造函数用来进一步操作:检查问题(是否开空间成功了)或者进行更加复杂的初始化(数组初始化可能要用到for循环)
- 能用初始化列表就用初始化列表(因为就算不用,他也会走初始化列表)(如果说这个自定义类型没有默认构造函数,那你就要么写一个,要么自己显示调用构造函数进行传参)
- 有一些比较复杂的成员变量就走函数体
Date(int year, int month, int day, int& x)
:_year(year)
,_month(month)
,_day(day)
,_n(1)
,_ref(x)
,_aa(1, 2)
,_p((int*)malloc(sizeof(4) * 10))
{
if (_p == nullptr)
{
perror("malloc fail");
}for (size_t i = 0; i < 10; i++)
{
_p[i] = 0;
}
}
//为了更好理解这个初始化列表,就尽量个平时的初始化类比一下就好了
int a = 3;-> :a(3)
:_year(year)
,_month(month)
,_day(day)
,_n(1)
,_ref(x)
,_aa(1, 2)
,_p((int*)malloc(sizeof(4) * 10))可以把这个当成函数的参数列表
新语法:单参数构造函数支持隐式类型转换
概念
如果说某个类支持用单个参数进行构造,就可以利用这个语法一步到位
Date(int month)
{
_month = month;
}Date d1 = 2;
//用2构造成一个临时对象(构造函数),再用这个临时对象拷贝给d1(拷贝构造)(因为是临时对象,所以具有常性,如果想用引用来接收的话要用常引用)
- 虽然上面看起来是构造+拷贝构造,但是编译器会对同一个表达式连续步骤的构造进行合二为一
新语法优点
- 对象进行给初值的时候更加方便了
- 声明可以给自定义类型缺省值了
class Date { public:Date(int year = 100) //单参数构造函数{year = _year;}void Print();public:int _year; };---------------------------------------------------------class Time { public:Time(Date d1){_d1 = d1;}private:Date _d1 = 3; //自定义类型:单参数构造函数支持隐式类型转换//Date _d1 = 3; ---> Date d3(3);Date _d1 = d3; };
- 如果说要构建一个栈,栈的元素是Date,就可以做到一下操作
原本:
Stack st;
Date d1(3);
st.Push(d1);现在:
Stack st;
st.Push(3);
C++11支持多参数构造函数的隐式类型转换
用花括号
Date d1 = {2022, 3, 21} //初始化
Date d1 = { 2022,3 ,21 }; //成员变量声明处赋初值
explicit(禁止构造函数隐式类型转换)
如果你不想让这种(单参数构造函数支持隐式类型转换)隐式类型转化,就可以用一个关键字:explicit
explicit Date(int month)
{
_month = month;
}
static成员
概念
被static用来修饰的成员属于整个类,相当于是类自己的全局变量
使用方法
- 类内声明,类外定义(要指定类域)(因为类里面的定义和初始化针对的是单独对象的,就不能走初始化列表,但是static成员也需要开空间定义初始化,所以就在类外定义)
- 访问方法:
如果是公有的,可以直接指定类域来访问: Date::count
如果是私有的,就用一个公有的成员函数去访问
作用
当全局变量来用,只不过这个全局变量的访问方式没那么直接,类似于用来构造统计
static成员函数
概念
一个不依赖于对象的成员函数,也没法访问某些对象的成员变量
特点:没有this
使用方法
- 在函数的最前面加上static即可
- 访问方法:
因为是公有的,并且不依赖于对象,所以可以直接指定类域来访问: Date::sta_Func()
虽说不依赖对象,但是对象也可以去调用它d1.sta_Func()
-
因为它不依赖对象,所以不能出现某些依赖于对象的成员变量,只能是有一些静态成员变量
作用
用来专门访问静态成员变量
虽然说可以直接让对象直接调用,但当函数逻辑不依赖对象状态(只需访问静态成员或纯参数计算)时,优先使用静态成员函数。这符合面向对象设计的"高内聚"原则,同时减少不必要的对象耦合
要将这些static和非static映射现实来理解,只有映射现实来理解,才不会因为少用而忘记他们具体的使用场景和作用
想象场景:一家汽车制造厂 (
Car
类)
静态成员变量
(static member variable
):工厂中央控制室的“总产量计数器”
它是什么? 工厂墙上挂着一个巨大的电子显示屏,上面显示着一个数字:当前工厂制造出的所有汽车的总数量。
属于谁? 这个计数器不属于任何一辆具体的汽车。它属于整个工厂本身。它是工厂运营的全局状态。
存在方式: 整个工厂只有一个这样的计数器。无论生产线上正在组装多少辆车,或者停车场里停了多少辆新车,这个计数器只有一个。
谁操作它?
非静态成员函数 (汽车个体的行为): 当一辆新车从生产线下线(对象被创建,构造函数调用)时,这辆新车会“通知”中央控制室:“我诞生了!”。控制室人员(或自动系统)就会把总产量计数器 +1。
当一辆汽车被销毁(对象被析构)时,它会“报告”:“我要报废了!”。控制室人员就把总产量计数器 -1。
静态成员函数 (工厂管理层的操作): 工厂经理可以直接走进中央控制室(无需通过任何一辆具体的汽车),查看这个计数器上的数字,了解当前总共有多少辆车存在 (
getTotalCars()
)。他甚至可以重置它(如果设计允许,但例子中通常不这样做)。关键点: 这个“总产量计数器”(静态变量)是工厂级别的全局信息,所有汽车个体(对象)共享并共同影响它,但它本身独立于任何一辆车。
非静态成员变量
:每辆汽车的“车辆识别号(VIN)”
它是什么? 每辆生产出来的汽车,在出厂时都会被打上一个独一无二的金属铭牌,上面刻着它的序列号(比如第0001号车,第0002号车...)。
属于谁? 这个序列号专属于这辆特定的汽车。它是这辆车的个体身份标识。
存在方式: 每一辆制造出来的汽车都有自己的序列号。停车场里有100辆车,就有100个不同的序列号。
谁操作它? 主要是非静态成员函数 (汽车个体的行为):
车主可以查看自己车的序列号 (
getSerial()
)。维修站可以通过序列号查询这辆车的保养记录。
这个序列号在汽车诞生时(构造函数中),由工厂根据当时的“总产量计数器”的值来赋予(
serialNumber = ++totalCarsCreated
)。一旦赋予,一般就不再改变。
静态成员函数
(static member function
):工厂汽车总数的查询
getTotalCars()
(静态成员函数):
定位: 属于
Car
类本身的全局操作。它的功能是“查询当前存在的Car
总数”。为什么是静态?
它不需要知道任何一辆具体车的信息(不需要访问
serialNumber
或对象的其他非静态成员)。它只关心类级别的状态
totalCarsCreated
。它可以在没有任何
Car
对象被创建的情况下被调用 (如main
函数开头),因为它操作的是类的全局状态,而非对象状态。调用方式清晰:
Car::getTotalCars()
明确表示这是类级别的信息。
非静态成员函数
:每辆汽车自带的“功能按钮”
它是什么? 每辆汽车上都有方向盘、油门、刹车、收音机按钮、车载显示屏等。
功能是什么? 这些按钮/功能操作的是这辆特定的汽车本身:
按方向盘是让这辆车转向 (
turnLeft()
)。踩油门是让这辆车加速 (
accelerate()
)。按收音机按钮是打开这辆车的音响 (
playRadio()
)。在车载显示屏上可以查看这辆车的序列号 (
getSerial()
) 或这辆车的油耗 (getFuelConsumption()
)。关键特点:
你必须“在车里”或“操作这辆车”: 要使用这些功能,你必须有具体的车对象 (
myCar.accelerate()
,yourCar.playRadio()
)。它们操作个体状态: 这些功能直接影响或查询的是你正在操作的那辆具体汽车的状态(速度、位置、音响开关、序列号等)。
它们也能“联系工厂”: 虽然主要功能是操作自身,但车上的某个按钮(比如一个特殊的“工厂信息”按钮)也可以用来查询工厂的总产量(调用静态函数
getTotalCars()
或访问静态变量totalCarsCreated
),就像车载系统可以拨打那个客户服务热线一样。这就是为什么非静态函数能访问静态成员。但它查询到的,仍然是整个工厂的信息,不是这辆车独有的。
友元
友元函数
-
定义在类的外部。
-
本身不是类的成员函数。
-
其目的是授予这个外部函数访问该类私有 (
private
) 和受保护 (protected
) 成员的权限。
使用方法
一个普通的在类外部的函数,然后在那个类里面去进行友元声明
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
pub1ic :
Date(int year = 1900, int month = 1, int day = 1)
: --year(year)
, _month(month)
,_day(day)}
语法
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰(函数用const修饰是用来修饰this的,仅仅是this指向的内容不能修改,友元函数就仅仅是一个普通的函数)
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同(就普通函数而已,访问私有不报错)
友元类
我这个类可能会在任何地方都要访问你的私有和保护,就可以在你的类里面声明友元
class Date
{
friend class Time;
pub1ic :
Date(int year = 1900, int month = 1, int day = 1)
: --year(year)
, _month(month)
,_day(day)}
友元是单向的,你可以访问他的私有,但他不能访问你的私有
内部类
概念
在一个类里面嵌套另一个类,两个类之间只是多了友元和域作用限定符的区别,还是两个单独的类
特性
- 内部类受到外部类的类域的限制(想定义B对象,要A::B bb(12, 32, 43);)
- 内部类受到外部类的访问限定符限制(private、protect、public)
- 内部类天生是外部类的友元(内部类可以访问外部类的私有,外部类不能访问内部类的私有)
匿名对象
没有名字的对象,即用即销毁
Date();
Date(10);
特性
生命周期只在当前一行
作用
如果你要调用Date里面的一个函数,你还得先创建一个Date对象出来才能调用
Date().Print(1);
匿名对象的作用重点在后面的仿函数
仅供了解:编译器对于构造的一些优化
构造+拷贝构造
拷贝构造+拷贝构造
不同的编译器激进程度不同;越激进越有可能出bug(原本该出现的,被优化掉就不出现了)
A aa1 = 2; 构造 + 拷贝构造 --》直接构造
返回值的情况下
拷贝构造+拷贝构造 -> 拷贝构造
aa->tmp->ret :: aa->ret,没有产生临时对象
匿名对象:f4()直接不在函数里产生对象,直接变成ret构造
并没有完整的结论,因为不同编译器的激进程度不一样,debug和release也不一样,release还更激进(在确保没有什么影响的情况下)
int main()
{A aa1 = 2; // 构造 + 拷贝构造 -》 直接构造const A& aa2 = 2;f1(aa1);f1(A(2)); // 构造 + 拷贝构造 -》 直接构造f2(aa1);return 0;
}A f4()
{return A();
}A f3()
{A aa;return aa;
}int main()
{// 拷贝构造+拷贝构造->拷贝构造A ret = f3();// 构造 + 拷贝构造 + 拷贝构造 ->构造//A ret = f4();//A ret;//ret = f4();return 0;
}