标题:【C -> Cpp】由C迈向Cpp(5)
@水墨不写bug
(图片来源于网络)
不抵制失败,携手失败,迈向成功
正文开始:
(一)深入理解构造函数
在之前的讲解中,我们已经对 不写会默认生成的 常用的 成员函数有了一个基本的认识,对于一个类而言,用一句话概括常用的成员函数的功能:
构造函数:完成对象的实例化(定义)。
析构函数:完成对象销毁前资源清理。
拷贝构造:在创建对象的同时完成赋值。
赋值重载:将一个对象赋值给另一个对象。
尽管我们已经对这些成员函数们有一个整体的认识了。然而,我们需要抓一抓构造函数的细节,以便于对它有一个更深的认识:
构造函数分为两部分--> 初始化列表和函数体
(1)初始化列表
使用方式:
跟在构造函数的函数头后,在函数体之前;
每一个成员变量都用 “ 变量名()”初始化;
第一个成员变量之前为 “ :”,成员变量之间用 “ , ”分隔。
示例:
#include <iostream> using namespace std; class Date { public: //构造函数 Date(int year = 0, int month = 0, int day = 0) :_year (year) ,_month (month) ,_day (day) {} private: int _year; int _month; int _day; };
构造函数有一个举足轻重的功能:初始化列表。
一般情况下,几乎所有 成员变量 都可以在初始化列表 初始化。
(成员变量不论是内置类型,还是自定义类型)
初始化列表是成员变量初始化的唯一方式。因为从本质上,在构造函数函数体内部对成员变量的赋值之后还可以多次改变,所以构造函数内对成员变量赋值不是初始化,而是赋初值。
不同对象的初始化方式:
1.引用成员变量、const成员变量、自定义类型的成员变量(这个自定义类型没有默认构造)这三类 必须在初始化列表初始化。
2.自定义类型对象和内置类型对象的初始化:
###初始化列表无论你写不写它总是存在的。当然,写初始化列表,并一个不漏的初始化是最好的情形。但是如果不写,可能就会发生一些意想不到的问题###
不写初始化列表——>
- 内置对象 在Cpp 语法上没有明确规定,取决于编译器的不同。有些编译器会对内置类型处理,不过大多数编译器不会对内置对象处理。
- 自定义对象 会调用其默认构造函数,如果调用默认构造函数失败,报出错误。
什么是调用默认构造函数失败?
/************************************/
要理解这个问题,我们先看看什么是默认构造?
- i, 自己没有写,编译器自动生成的的构造函数是无参数的构造函数,也就是默认构造;
- ii,自己写了一个函数,但是没有参数,也可以被编译器识别为默认构造函数,这时编译器就不会再自动生成一个构造函数了;
- iii,自己写了一个函数,有参数,但是参数全缺省,这也可被编译器识别为默认构造函数,这时编译器就不会再自动生成一个构造函数了;
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/2301_79465388/article/details/138410081/***********************************/
也就是说明:
如果没有默认构造,并且编译器也无法生成时,就会调用默认构造函数失败。
3.成员变量的缺省值是对初始化列表的补充。
由于不写初始化列表——>大多数编译器不会对内置对象处理。所以C++11新增成员变量的缺省值,缺省值的给出是 非常灵活的,请看如下示例中的 P类。
示例:
#include<iostream> #include<cstdlib> using namespace std; class Stack { public: Stack() { int* tem = (int*)realloc(_arr, sizeof(int) * n); if (tem == nullptr) { perror("realloc fail"); exit(-1); } _arr = tem; } private: int _top = 0; int* arr = (int*)malloc(sizeof(int)*4); }; class P { public: P() {} double re_Pi() { return 3.1415926; } private: /*成员默认值给出是非常灵活的*/ //可以是常量值 int _a = 0; char _c = 'c'; //常量表达式 long _b = 8 + 3; //函数返回值 double pi = re_Pi(); //动态申请的空间指针 int* ptr = (int*)malloc(sizeof(int)*4); //另一个对象并调用构造 Stack st = 10; };
通过单步调试,我们可以观察的很清楚 :
1.首先进入P的构造函数,准备进入初始化列表(没有写也是存在的)
2.进入初始化列表,用默认值初始化成员变量。
默认值为函数返回值也是可以正常进入的:
对于栈的初始化也是可以正常找到默认构造的:
初始化完成:
4,成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
示例:/*这个程序的输出结果是什么*/ 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(); }
其实结果是输出: { 1 随机值 }
(因为初始化列表是根据成员变量声明顺序来初始化的,由于先声明_a2,所以先初始化_a2,这时_a1还是随机值,所以_a2初始化是被随机值初始化的,是无效的)
构造函数的初始化列表总结:
&能在初始化列表的尽量都在初始化列表初始化,如果实在不能再考虑在构造函数的函数体内进行赋值&
(2)explicit关键字
用explicit修饰构造函数,将会禁止构造函数的隐式转换
构造函数 其实做了比我们想象的更多的事:
class Date
{
public:
// 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用
explicit Date(int year)
:_year(year)
{}
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(2024);
d1 = 2023;
return 0;
}
2023赋值给d1,构造函数则完成了将整形值隐式类型转换成Date类的临时对象,然后通过赋值重载将临时对象的值赋值给d1。
1.对于任意的单参数的构造函数,当我们传入的类型和对象类型不一致时,都会发生这一转换。
什么是单参数?
- 1. 构造函数只有一个参数
- 2. 构造函数有多个参数,除第一个参数没有默认值外,其余参数都有默认值
- 3. 全缺省构造函数
这也是构造函数让我们用的很方便的原因。
2.如果在构造函数之前加上explicit,就禁止了隐式类型转换。
这样,编译会报错:
加上 explicit之后无法完成类型转换:
d1 = 2023;
编译报错
(图片来源于网络)
完~