<C++>二、类和对象-构造函数

1.类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类。

空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

2bb2888a284f040286e4c4ce432e13ea.png

2.构造函数

构造函数是一个特殊的成员函数名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。

构造函数的目的就是为了创建对象的时候同时初始化。

其特征如下:

1.函数名与类名相同。
2.无返回值
3.对象实例化时编译器自动调用对应的构造函数。也就是说在创建对象的时候,就对成员变量进行初始化。

下面是一个Stack栈的类:


class Stack
{
public://无参数构造函数 - 函数名与对象名相同Stack(){_a = nullptr;_size = _capacity = 0;}//带参数构造函数Stack(int n){_a = (int*)malloc(sizeof(int) * n);if (nullptr == _a){perror("malloc fail");exit(-1);}_capacity = n;_size = 0;}void Push(int x) {}void Destroy() {}
private:int* _a;int _size;int _capacity;
};
6eed38839bf21f69ace16a24e1d9bf3d.png
4.构造函数可以重载。(一个类可以有多个构造函数,也就是多种初始化方式)

//Date类举例
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 << _year << "/" << _month << "/" << _day << endl;}
private:int _year;int _month;int _day;
};
5c7f4e8d953771168b98db62260cc725.png

使用全缺省构造函数

在写构造函数的时候,推荐使用全缺省或者半缺省


class Date
{
public://全缺省Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << "/" << _month << "/" << _day << endl;}
private:int _year;int _month;int _day;
};
982f189a5b82c5a671a3eab06a0d0279.png

全缺省构造函数不能和无参构造函数同时存在 - 出现歧义,编译器不知道调用哪个

5.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。

为什么只能有一个默认构造函数? 因为调用的时候会发生歧义

不传参数就可以调用构造函数,一般建议每个类都提供一个默认构造函数

e90a1960b3dbb0b8ec350de5d54f9f1b.png
6.如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
938304bb86094e0c4aff5e98af2ebdee.png

为什么自动生成了默认构造函数,打印出来的变量还是随机值呢?

C++规定默认生成的构造函数:

1.内置类型成员不做处理
2.自定义类型的成员,会去调用自定义类型的类的构造函数。
注意:C++11中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。

默认构造函数的场景:

比如用栈实现队列:


//Stack类
class Stack
{
public://无参构造函数 - 函数名与对象名相同Stack(){cout << "Stack()" << endl;_a = nullptr;_size = _capacity = 0;}//带参数构造函数Stack(int n){cout << "Stack()" << endl;_a = (int*)malloc(sizeof(int) * n);if (nullptr == _a){perror("malloc fail");exit(-1);}_capacity = n;_size = 0;}void Push(int x) {}void Destroy() {}~Stack()  //析构函数{cout << "~Stack()" << endl;free(_a);_a = nullptr;_size = _capacity = 0;}
private:int* _a;int _size;int _capacity;
};
470730d0a0b989c37d0cd01cbc844484.png

可以看到MyQueue中没有构造函数,它会自动调用Stack类中的构造函数。

2.1构造函数体赋值

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。


class Date
{
public:
Date(int year, int month, int day){_year = year;_month = month;_day = day;}
private:
int _year;
int _month;
int _day;
};

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。

2.2 初始化列表

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟 一个放在括号中的初始值或表达式。


class Date
{
public:Date(int year, int month, int day): _year(year), _month(month), _day(day){}private:int _year;int _month;int _day;
};
【注意】
1.每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2.类中包含以下成员,必须放在初始化列表位置进行初始化:
① 引用成员变量
② const成员变量
③ 自定义类型成员(且该类没有默认构造函数时)

class B
{
public:B(int):_b(0){cout << "B()" << endl;}
private:int _b;
};class A
{
public://1、哪个对象调用构造函数,初始化列表是它所有成员变量定义的位置//2、不管是否显示在初始化列表写,那么编译器每个变量都会初始化列表定义初始化//3、三个成员变量需要写初始化,一个是const变量,一个引用变量,一个自定义类型A()   //这是一个初始化列表:_x(1),_ref(_a1), _a2(1),_bb(0){_a1++;_a2--;}
private:int _a1 = 1;  // 声明int _a2 = 2;   //缺省值是在没有构造函数的时候,缺省值才有效 //const变量必须在定义的位置初始化,因为后面const变量就无法更改了//const int _x;   //加了这个const  编译器会报错,无法引用A的默认构造函数//const int _x = 1;  //C++11 可以这么给const int _x;int& _ref;B _bb;
};int main()
{A aa;   // 对象整体的定义,每个成员什么时候定义呢?//必须给每个成员变量找一个定义的位置,不然像const这样的成员,没办法初始化return 0;
}
3.尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量, 一定会先使用初始化列表初始化。

class Time
{
public:Time(int hour = 0): _hour(hour){cout << "Time()" << endl;}private:int _hour;
};class Date
{
public:Date(int day){}private:int _day;Time _t;    //调用Time的构造函数 ,打印Time();
};int main()
{Date d(1);
}
4.成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

class A
{
public:A(int a): _a1(a), _a2(_a1){}void Print(){cout << _a1 << " " << _a2 << endl;}private:int _a2; //_a2先初始化,_a1在初始化   此时调用初始化列表,而_a1此时是随机值,所以_a2(_a1)后_a2为随机值int _a1;
};int main()
{A aa(1);aa.Print();
}/*     D
A.输出1 1
B.程序崩溃
C.编译不通过
D.输出1 随机值
*/

2.3 explicit关键字

构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。


class Date
{
public://1.单参构造函数,没有使用explicit修饰,具有类型转换作用Date(int year) : _year(year) {}// 2,虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转换作用 - int转换为Date类Date(int year, int month = 1, int day = 1): _year(year), _month(month), _day(day) {}
private:int _year;int _month;int _day;
};int main()
{Date d1(2022);d1 = 2023;//用一个整型变量给日期类型对象赋值//实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值return 0;
}

class Date
{
public:// explicit修饰构造函数,禁止类型转换explicit Date(int year, int month = 1, int day = 1): _year(year), _month(month), _day(day) {}private:int _year;int _month;int _day;
};int main()
{Date d1(2022);d1 = 2023;  //err 编译器报错,没有与这些操作数匹配的 "=" 运算符,操作数类型为:  Date = intreturn 0;
}

class Date
{
public:Date(int year) : _year(year), _month(1), _day(1) {}friend ostream& operator<<(ostream& out, const Date& d);// operator是重载运算符,后面了解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;
};ostream& operator<<(ostream& out, const Date& d)
{cout << d._year << " " << d._month << " " << d._day << endl;return out;
}int main()
{Date d1(2022); // 构造函数d1 = 2023; // 隐式类型转换  构造+拷贝+优化->构造  有的编译器可能不会优化// 构造,调用Date(year),在拷贝调用的是operator// Date& ref = 10;  //err ref是类类型 不能引用10const Date& ref = 10; // 可以,隐式类型转换,10构造一个对象,临时对象具有常属性,所以const可以引用cout << ref << endl;   //10 1 1return 0;
}
用explicit修饰构造函数,将会禁止构造函数的隐式转换。

3.析构函数

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

其特征如下:

1.析构函数名是在类名前加上字符 ~。
2.无参数无返回值类型。
3.一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4.对象生命周期结束时,C++编译系统系统自动调用析构函数。
5.关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定义类型成员调用它的析构函数。(跟构造函数类似)
6.如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类
7.析构函数的执行顺序是先创建的对象后析构,后创建的对象先析构

下面是几个可能需要使用析构函数的场景:

1.动态分配内存:当一个对象在堆上分配内存时,需要手动释放内存以避免内存泄漏。在对象的析构函数中释放分配的内存是一种常见的方法。

class MyClass {
public:MyClass() {data = new int[100];}~MyClass() {delete[] data;}
private:int* data;
};
2.异常处理:如果在构造函数中发生了异常,那么对象可能没有完全初始化,因此析构函数应该只释放已经成功初始化的资源。

class MyClass {
public:MyClass() {// 可能会抛出异常data = new int[100];// 如果抛出异常,data指针将保持为nullptr}~MyClass() {if (data != nullptr) {delete[] data;}}
private:int* data = nullptr;
};

4.拷贝构造函数

概念:

拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

拷贝构造函数也是特殊的成员函数,其特征如下:

1.拷贝构造函数是构造函数的一个重载形式。
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。

为什么需要拷贝构造呢?

因为编译器只能对内置类型进行直接拷贝,而自定义类型的拷贝,需要使用拷贝构造

09f30d90803d892972981dd6b3150368.png

Date类中的成员变量都是内置类型,编译器可以直接拷贝。而Stack栈中的a是指针,如果直接拷贝,st1和st2指向了同一块内存地址,当st2进行析构处理的时候,也就把st1的a也清理了,当st2push一个1,st1也会跟着push一个1,所以对于自定义不能直接拷贝。

为什么拷贝构造使用传值调用会发生无穷递归?


//传值调用
Date(Date d){_year = d._year;_month = d._month;_day = d._day;}
20ca443f4b000ecc142ab43dc5aed985.png

//正确的写法
class Date
{
public:Date(int year = 2023, 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;}void Print(){cout << _year << "/" << _month << "/" << _day << endl;}
private:int _year;int _month;int _day;
};int main()
{Date d1(2021, 2, 3);Date d2(d1);d2.Print();return 0;
}
817c4ed378097a9200851bfb078212cd.png
3.若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

默认生成拷贝构造和赋值重载:

a、内置类型完成浅拷贝/值拷贝--按byte一个一个拷贝
b、自定义类型,去调用这个成员拷贝构造/赋值重载

//自动生成构造拷贝函数对内置类型进行拷贝
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << "/" << _month << "/" << _day << endl;}private:int _year;int _month;int _day;
};
ec23b44cecffb8850a29fd63ca0e65d4.png
4.编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗? 当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?

//自动生成构造拷贝函数对自定义类型进行拷贝
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 10){cout << "Stack(size_t capacity = 10)" << endl;_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申请空间失败");exit(-1);}_size = 0;_capacity = capacity;}void Push(const DataType& data){// CheckCapacity();_array[_size] = data;_size++;}~Stack(){cout << "~Stack()" << endl;if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}private:DataType* _array;size_t    _size;size_t    _capacity;
};
99a5c008f6222e789cc241f21b2a33ae.png

这里会发现上面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。

5d80bb2b8cb17c7270bcd10a63368624.png

注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

5.拷贝构造函数典型调用场景:

a、使用已存在对象创建新对象

b、函数参数类型为类类型对象

c、函数返回值类型为类类型对象


typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 10){cout << "Stack(size_t capacity = 10)" << endl;_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申请空间失败");exit(-1);}_size = 0;_capacity = capacity;}void Push(const DataType& data){// CheckCapacity();_array[_size] = data;_size++;}//stack类的拷贝构造深拷贝Stack(const Stack& st){cout << "Stack(const Stack& st)" << endl;//深拷贝开额外空间,为了避免指向同一空间_array = (DataType*)malloc(sizeof(DataType)*st._capacity);if (nullptr == _array){perror("malloc申请空间失败");exit(-1);}//进行字节拷贝memcpy(_array, st._array, sizeof(DataType)*st._size);_size = st._size;_capacity = st._capacity;}~Stack(){cout << "~Stack()" << endl;if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}private:DataType *_array;size_t    _size;size_t    _capacity;
};class MyQueue
{
public://MyQueue什么都不写,会调用默认的构造函数,也就是Stack类的构造函数// 默认生成构造// 默认生成析构// 默认生成拷贝构造private://默认构造函数初始化 - 默认析构函数Stack _pushST;//默认构造函数初始化 - 默认析构函数Stack _popST;int _size = 0;
};int main()
{Date d1(2023, 2, 5);d1.Print();Date d2(d1);Date d3 = d1; //  拷贝构造d2.Print();Stack st1;st1.Push(1);st1.Push(2);st1.Push(4);Stack st2(st1);cout << "=============================" << endl;MyQueue q1;//q1拷贝q2  q1中有两个Stack类和一个size,size直接拷贝,stack类是调用stack拷贝构造进行拷贝MyQueue q2(q1);return 0;
}

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/17642.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Linux多线程】死锁问题介绍

死锁 &#x1f96d;什么是死锁&#x1f965;死锁产生的条件&#x1f95d;如何避免死锁&#x1f345; 避免死锁算法 &#x1f96d;什么是死锁 死锁是多线程或多进程编程中的一种常见问题&#xff0c;指的是两个或多个线程&#xff08;或进程&#xff09;相互等待对方持有的资源&…

位操作相关的函数(C++)

目录 popcount函数 bitset类模板 __builtin_popcount函数 popcount函数 在C中&#xff0c;std::popcount函数是用来计算一个整数二进制表示中包含的1的个数。不过要注意&#xff0c;这个函数是C20标准引入的&#xff0c;因此在使用之前&#xff0c;要先确保编译器支持C20标…

Redis 理论部分

前面写了很多redis项目&#xff0c;今天在通过redis的理论加深redis的了解&#xff0c;顺便做个总结 Redis 理论部分 1.redis 速度快的原因 纯内存操作单线程操作&#xff0c;避免频繁的上下文切换以及资源争用的问题&#xff0c;多线程需要占用更多的cpu资源采用非阻塞I/O多…

搜索二叉树_SearchBinaryTree

目录 搜索二叉树的原理 搜索二叉树的搜索时间复杂度 二叉搜索树实现_key 模型 节点 构造函数 查找 中序遍历 插入 循环 递归 删除 循环 1.删除叶子节点 2.删除有一个孩子的节点 3.左右孩子都不为空 递归 析构函数 拷贝构造 operator key_value 模型 节点 …

Mysql触发器

1.触发器 触发器是与表有关的数据库对象&#xff0c;指在 insert / update / delete 之前或之后&#xff0c;触发并执行触发器中定义的SL语句集合。触发器的这种特性可以协助应用在数据库端确保数据的完整性&#xff0c;日志记录&#xff0c;数据校验等操作。 使用别名 OLD 和 …

C#常用数学插值法

目录 1、分段线性插值 2、三次样条插值 3、拉格朗日插值 &#xff08;1&#xff09;一元全区间不等距插值 &#xff08;2&#xff09;一元全区间等距插值 4、埃尔米特插值 &#xff08;1&#xff09;埃尔米特不等距插值 &#xff08;2&#xff09;埃尔米特等距插值 1、…

mysql安装教程保姆级

MySQL免安装本地运行 1.下载MySQL2.创建install.bat3.init.sql 初始创建4.环境变量配置5.运行 install.bat 管理员权限运行6.连接成功遇到的问题 1.下载MySQL ①地址&#xff1a;https://downloads.mysql.com/archives/community/ ②解压 2.创建install.bat 放在mysql>b…

【SpringBoot笔记37】SpringBoot基于@ServerEndpoint、@OnMessage等注解的方式集成WebSocket

这篇文章,主要介绍SpringBoot基于@ServerEndpoint、@OnMessage等注解的方式集成WebSocket。 目录 一、基于注解集成WebSocket 1.1、WebSocket常见注解 1.2、创建WebSocket服务端 1.3、配置ServerEndpointExpor

java数据算法-汉诺塔

1、有三根相邻的柱子&#xff0c;标号为A,B,C。 2、A柱子上从下到上按金字塔状叠放着n个不同大小的圆盘。 3、现在把所有盘子一个一个移动到柱子C上&#xff0c;并且每次移动同一根柱子上都不能出现大盘子在小盘子上方。 题解步骤 1、当n1时&#xff1b; 将1号从A移动到C即…

算法综合篇专题一:双指针问题

"就算没有看清那株灿烂的花蕊&#xff0c;也应该放声歌颂赞美鲜红的玫瑰" 1、移动零 (1) 题目解析 (2) 算法原理 class Solution { public:void moveZeroes(vector<int>& nums) {for(int cur0,dest-1;cur<nums.size();cur){if(nums[cu…

AcWing 4908.饥饿的牛

原题链接&#xff1a;AcWing 4908.饥饿的牛 题目来源&#xff1a;夏季每日一题2023 贝茜是一头饥饿的牛。 每天晚上&#xff0c;如果牛棚中还有干草的话&#xff0c;贝茜都会吃掉其中的一捆。 初始时&#xff0c;牛棚中没有干草。 为了让贝茜不被饿死&#xff0c;农夫约翰制…

AOJ 2200 Mr. Rito Post Office 最短路径+动态规划+谨慎+思维

我写了好多注释&#xff0c;一看就能看懂&#xff0c;这个题目我想了6&#xff0c;7个小时&#xff0c;一开始忽略了船的位置和要把船安置的位置一致的情况&#xff0c;补上就对了。 #include <iostream> using namespace std; int inf 0x3f3f3f3f, num[1007], dp[1007…

Ansible-roles

Ansible-roles 一、roles作用 把playbook剧本里的各个play看作为角色&#xff0c;将各个角色的tasks任务、vars变量、templates模板、files文件等内容放置到角色的目录中统一管理&#xff0c;需要的时候可在playbook中直接使用roles调用&#xff0c;所以roles可以实现playboo…

实验报告6-利用Modelsim搭建一个UVM验证平台并跑通程序显示波形

实验报告6-利用Modelsim搭建一个UVM验证平台并跑通程序显示波形 1,背景知识2,搭建一个UVM验证平台3,确定几个重要组件的代码,全部都是.sv文件。(1)确定了行为,写interface文件:pkt_if.sv,代码如下:(2)pkt_data.sv(3)pkt_drv.sv(4)pkt_gen.sv(5)environment.s…

【微信小程序】引入第三方库poke对GZIP压缩数据进行解压

使用 npm 包管理工具&#xff1a; 首先&#xff0c;在小程序的根目录下执行 npm init 初始化项目&#xff0c;生成 package.json 文件。然后&#xff0c;通过 npm 安装 pako&#xff1a;npm install pako。接下来&#xff0c;在小程序的根目录下创建一个名为 miniprogram_npm 的…

java设计模式-建造者(Builder)设计模式

介绍 Java的建造者&#xff08;Builder&#xff09;设计模式可以将产品的内部表现和产品的构建过程分离开来&#xff0c;这样使用同一个构建过程来构建不同内部表现的产品。 建造者设计模式涉及如下角色&#xff1a; 产品&#xff08;Product&#xff09;角色&#xff1a;被…

前端工程化最佳实践:项目结构、代码规范和文档管理

文章目录 前端工程化最佳实践项目结构设计与组织文档管理和注释规范国际化和本地化实践 前端工程化的未来发展趋势前端工程化领域的最新技术和工具WebAssembly 和前端性能优化可持续性和可访问性的趋势 总结前端工程化的关键知识点前端工程化对项目和团队的价值 前端工程化最佳…

《golang设计模式》第一部分·创建型模式-01-单例模式(Singleton)

文章目录 1. 概述1.1 目的1.2 实现方式 2. 代码示例2.1 设计2.2 代码 1. 概述 1.1 目的 保证类只有一个实例有方法能让外部访问到该实例 1.2 实现方式 懒汉式 在第一次调用单例对象时创建该对象&#xff0c;这样可以避免不必要的资源浪费 饿汉式 在程序启动时就创建单例对象…

卷积神经网络

目录 注意&#xff1a;有参数计算的才叫层 1.应用 1.1分类和检索 1.2超分辨率重构 1.3医学任务 1.4无人驾驶 1.5人脸识别 2.卷积 2.1卷积神经网络和传统网络的区别 2.2整体框架 2.3理解卷积&#xff08;重点&#xff09; 2.4为何要进行多层卷积 2.5卷积核的参数 2.6…

C++STL库中的list

文章目录 list的介绍及使用 list的常用接口 list的模拟实现 list与vector的对比 一、list的介绍及使用 1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器&#xff0c;并且该容器可以前后双向迭代。 2. list的底层是双向带头循环链表结构&#xff0c;双向带头循…