IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 【C++】类和对象——构造函数 -> 正文阅读

[C++知识库]【C++】类和对象——构造函数

什么是构造函数

对于一个普通的日期类:

class Date {
public:
    void Init(int year, int month, int day) {
        _year = year;
        _month = month;
        _day = day;
    }
    
    void Display() {
    	cout <<_year<< "-" <<_month << "-"<< _day <<endl;
    }
    
private:
    int _year;
    int _month;
    int _day;
};

int main() {
    Date d1,d2;
    d1.Init(2022, 8, 14);
    d1.Display();
    Date d2;
    d2.Init(2022, 8, 15);
    d2.Display();
    return 0;
}

我们专门写了一个 Init 初始化函数来完成对对象的初始化,但这样是否有些多余了呢?

C++提供了一个特殊的成员函数——构造函数,构造函数的名字与类名相同没有返回类型创建自定义类的对象时由编译器自动调用,且在对象的创建到销毁只调用一次,构造函数我们如果不写编译器会自动生成一个。

注意:构造函数也是可以重载的!

下面给 Date 类写一个构造函数:

class Date {
public:
    Date(int year, int month, int day) {
        _year = year;
        _month = month;
        _day = day;
    }
    
    void Display() {
    	cout <<_year<< "-" <<_month << "-"<< _day <<endl;
    }
    
private:
    int _year;
    int _month;
    int _day;
};

int main() {
    Date d1(2022, 8, 15);
    // Date d1(&d1, 2022, 8, 15)
    return 0;
}

默认构造函数

像上面刚刚写的 Date 类的构造函数,有一点很麻烦,就是创建对象时必须要传参数:

image-20220815195714383

Date d1(); 这种也算传参数,只是传的参数为空(void),而不传参数的意思指Date d1;这种。

错误说明中提到了“默认构造函数”,那什么是默认构造函数呢?

通俗一点来理解,就是已经默认给好参数了,不需要你传参就能完成对象的初始化。

像这种不传参也能初始化对象的默认构造函数有三种,分别是无参构造函数全缺省构造函数我们不写编译器自动生成的构造函数

下面分别演示一下:

class Date {
public:
    Date() {
        _year = 2022;
        _month = 8;
        _day = 15;
    }
    
    void Display() {
    	cout <<_year<< "-" <<_month << "-"<< _day <<endl;
    }
    
private:
    int _year;
    int _month;
    int _day;
};

int main() {
    Date d1;
    d1.Display();
    return 0;
}

这种就属于无参构造函数,运行结果如下:

image-20220815231958547

class Date {
public:
    Date(int year = 2022, int month = 8, int day = 15) {
        _year = year;
        _month = month;
        _day = day;
    }
    
    void Display() {
    	cout <<_year<< "-" <<_month << "-"<< _day <<endl;
    }
    
private:
    int _year;
    int _month;
    int _day;
};

int main() {
    Date d1;
    d1.Display();
    return 0;
}

这种就属于全缺省构造函数,运行结果如下:

image-20220815232037967

class Date {
public:
    void Display() {
    	cout <<_year<< "-" <<_month << "-"<< _day <<endl;
    }
    
private:
    int _year;
    int _month;
    int _day;
};

int main() {
    Date d1;
    d1.Display();
    return 0;
}

这种我们没写构造函数,但编译器自动会生成一个,运行结果如下:

image-20220815232152775

这时就有问题了,打印出来的都是随机值。

这是因为像 int、char、double…这些属于内置类型,像我们用 struct、class 等定义出来的类就属于自定义类型,编译器自动生成的默认构造函数对内置类型不作处理,而对自定义类型则会自动去调用它的构造函数

可以看下面的例子:

class Time {
public:
	Time() {
		cout << "Time()" << endl;
	}

private:
	int _hour;
	int _minute;
	int _second;
};

class Date {
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t;
};

int main() {
	Date d;
	return 0;
}

运行结果如下:

image-20220815232842677

总结一下,只有上述三种函数才是默认构造函数,而这三种函数显然是构成重载的,所以在写代码的时候需要注意,一般默认构造函数都推荐用全缺省的。


初始化列表

在讲初始化列表之前先想一下,如果成员变量中有 const 类型的变量,或者是引用,或者是没有默认构造函数的自定义类型,比如下面这样:

class A {
public:
    A(int a) {
    	_a = a;
    }
    
private:
	int _a;
};

class B {
public:
    
private:
    A _aa; // 没有默认构造函数
    int& _ref; // 引用
    const int _n; // const
};

此时写一个 B 的构造函数,比如下面这个:

B(int aa, int& ref, int n, int& tmp) {
    _aa(aa);
    _ref = ref;
    _n = n;
    _ref = tmp;
}

image-20220816185459429

错误百出。

因为构造函数是在创建对象时由编译器自动调用的,哪有这里的_aa(aa)这种写法?

再者,_ref 一开始是 ref 的别名,后来是把它变成 tmp 的别名了吗?其实是不经意间修改了 ref 的值!

还有,const 常变量是不能被修改的,只能在定义时就给好,而这里是赋值,不是定义时的初始化。

初始化只能初始化一次,而函数体内却可以多次赋值

而解决这一问题的方法就是使用初始化列表。

还是以上面的 B 类为例,用初始化列表的方式写就是这样:

B(int a, int& ref, int n)
    :_aobj(a)
    ,_ref(ref)
    ,_n(n)
{}

这里就不运行了,参数传的没问题就可以。

解释一下,初始化列表是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个“成员变量”后面跟一个放在括号里的初始值或表达式,如果是自定义类型则根据自定义类型的构造函数拷贝构造函数传入参数。

像我们不写编译器默认给的构造函数在处理自定义类型时会调用自定义类型的默认构造函数,实际上就是在初始化列表调用的,这也是为什么自定义类型成员变量没有默认构造函数时编不过的原因。

但是初始化列表中的初始化顺序也是有细节的,成员变量在初始化列表中的初始化次序是其在类中的声明次序与其在初始化列表中的先后次序无关

例如:

class A {
public:
	A(int a) {
		cout << "A(int a)" << endl;
		_a = a;
	}
	
private:
	int _a;
};

class B {
public:
    B(int b) {
        cout << "B(int b)" << endl;
     	_b = b;
    }
    
private:
    int _b;
};

class C {
public:
    C(int a, int b)
        :_A(a)
        , _B(b)
        {}
    
private:
    B _B;
    A _A;
};

int main() {
    C cc(1, 2);
    return 0;
}

运行结果如下:

image-20220816191056791
补充一点,初始化列表是成员变量初始化的地方,无论我们写不写,在定义对象时都会走一遍,包括前面提到的编译器自动生成的构造函数对内置类型不处理而对自定义类型去调用它的默认构造函数,原因就是初始化列表的作用。


通过构造函数发生隐式转换的条件

有下面一段代码:

class A {
public:
	A(int a = 1)
		:_a(a)
	{}

	A& operator+= (A AA) {
		_a += AA._a;
		return *this;
	}
    
    void Print() {
        cout << _a << endl;
    }

private:
	int _a;
};

int main() {
	A _A1(1);
    A _A2(2);
    
    _A1 += _A2;
    _A1.Print();
    
	_A1 += 1;
    _A1.Print();
	return 0;
}

其中 operator+= 是运算符重载函数,this 指针默认是左操作数,传过来的实参为右操作数,可以实现自定义类型之间的运算。

运行结果如下:

image-20220816193132553

对于第一次 += 没什么疑问,确确实实是两个类的运算。

但第二次一个类 += 一个整数,却没有报错,结果还正确,这就说明发生了隐式类型转换,将 int 隐式转换成了 A 类,在隐式转换的过程中调用了一次构造函数,将 1 作为构造函数的参数。

为了验证这点,我们在构造函数的函数体中添加一句cout << "A(int a = 1)" << endl;作为标记,再次运行程序,结果如下:

image-20220816193551887

这一点是我在一篇博客中看到的,那篇博客专门讲了单参数构造函数的隐式类型转换,但这个单参数的意思是构造函数只有一个参数吗?

再看下面一段代码:

class A {
public:
	A(int a1, int a2 = 1)
		:_a1(a1)
		,_a2(a2)
	{
		cout << "A(int a1, int a2 = 1)" << endl;
	}

	A& operator+= (A AA) {
		_a1 += AA._a1;
		_a2 += AA._a2;
		return *this;
	}

	void Print() {
		cout << "_a1 = " << _a1 << " ";
		cout << "_a2 = " << _a2 << endl;
	}

private:
	int _a1;
	int _a2;
};

int main() {
	A _A1(1, 0);
	_A1.Print();

	_A1 += 1;
	_A1.Print();
	return 0;
}

运行结果如下:

image-20220816194251959

这是构造函数有两个参数,但是只需要传一个参数就可以正常调用,这时发生了隐式转换,与上面情况相同。

可见,想要通过构造函数发生隐式转换的条件是只需要给构造函数传一个参数

如果想要避免这种隐式转换的发生也很简单,有两种方法:

  1. 以第一段给出的代码为例,那里 operator+= 的参数类型是 A,如果把参数类型改为 ***A&***,就不会发生隐式转换,因为传过来的参数是一个 A 类的引用类型,没有实体操作数可以转换成引用类型

    image-20220816194942555

  2. 第二种方法就比较粗爆了,就是用 explicit 关键字修饰构造函数:

    image-20220816195108103


C++11的成员初始化新玩法

C++11支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变
量缺省值

由于编译器默认给的构造函数不对内置类型进行处理,C++11就有一个新玩法可以弥补这一缺陷,玩法如下:

class A {
public:
    void Print() {
        cout << _a << endl;
    }
    
private:
    int _a = 0;
};

int main() {
    A _A;
    _A.Print();
    return 0;
}

运行结果如下:

image-20220816195740444

再次重申,这里不是初始化,而是给声明的成员变量缺省值

尽管C++11支持这样的用法,但实际中还是更推荐我们自己写构造函数,用初始化列表去完成初始化,这样还是最安全的。

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-08-19 18:45:50  更:2022-08-19 18:47:30 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/10 4:09:34-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码