本专栏目的
- 更新C/C++的基础语法,包括C++的一些新特性
前言
- 周末休息了,没有更新,请大家见谅哈;
- 构造函数、析构函数可以说便随着C++每一个程序,故学构造函数、析构函数是必要的;
- C语言后面也会继续更新知识点,如内联汇编;
- 本人现在正在写一个C语言的图书管理系统,1000多行代码,包含之前所学的所有知识点,包括链表和顺序表等数据结构,请大家耐心等待!!预计国庆前写完更新。
文章目录
- 构造函数
- 析构函数
- 构造/析构函数调用机制
- 析构函数调用时间
- 构造/析构函数用途展示
- 构造函数分类
- 无参构造函数
- 有参构造函数
- 拷贝构造函数(赋值构造)
- 移动构造函数
- 深拷贝和浅拷贝
- 构造函数的初始化参数列表
- 初始化参数列表
- 类中类如何构造
构造函数
首先我们写一个学生类,定义一个公有函数,用来打印学生信息:
#include <iostream>
class Student
{
public:
void print()
{
std::cout << "学号: " << m_uid << " 姓名: " << m_name << " 年龄: " << m_age << std::endl;
}
private:
std:string m_uid;
std::string m_name;
int m_age;
}
int main() {
Student stu;
stu.print();
}
这个时候,你肯定有一个疑问,创建出的这个学习信息,没有赋值!!!,那怎么赋值呢?这个时候你可能会想到在类中再定义一个API函数进行赋值,如下:
void setMessage(std::string uid, std::string name, int age) {
m_uid = uid;
m_name = name;
m_age = age;
}
这样确实能够解决问题,那如果每次都要这样,不觉得太麻烦了么?,每次创建类都要额外在调用一个函数!!!
因此,C++大叔也考虑到了这一点,及发明出了构造函数这个“简单”的东西,它允许我们再创建对象的时候自动调用该函数,如我们将上面的学生类进行修改:
#include <iostream>
class Student
{
public:
// 方法一
Student(std::string uid, std::string name, int age)
{
m_uid = uid;
m_name = name;
m_age = age;
}
// 方法二,推荐:参数列表方法
Student(std::string uid, std::string name, int age)
:m_uid(uid),
m_name(name),
m_age(age)
{
}
void print()
{
std::cout << "学号: " << m_uid << " 姓名: " << m_name << " 年龄: " << m_age << std::endl;
}
private:
std:string m_uid;
std::string m_name;
int m_age;
}
int main() {
Student stu("123456", "wy", 18); // 创建时候赋值
stu.print();
}
这样写无论从逻辑上,还是再写代码简约上,都好很多。
构造函数特点:
- 构造函数名和类名相同
- 构造函数可以重载
- 构造函数没有返回类型声明
调用:
- 自动调用(隐式),一般默认情况下C++编译器会自动调用构造函数(无参构造)
- 手动调用(显示),在一些情况下则需要手工调用构造函数(有参构造)
析构函数
当对象释放时,我们可能需释放/清理对象里面的某些资源,如果再类中对某一个变量,如:成员变量申请了一块内存,而在应对稍微复杂一点的项目,就很容易忘记释放内存,为了解决这个问题,C++提供了析构函数来处理对象的清理工作。析构函数和构造函数类似,不需要用户来调用它,而是在释放对象时自动执行。
特点:
- 析构函数名和类名相同,但是得在前面加一个波浪号**~**
- 析构函数只能有一个
- 构造函数没有返回类型声明
构造/析构函数调用机制
当定义了多个对象时,构造与析构的顺序是怎么样的呢?
#include <iostream>
using namespace std;
class Test
{
public:
Test(int id)
:m_id(id)
{
cout << m_id << " " << __FUNCTION__ << endl;
}
~Test()
{
cout << m_id << " " << __FUNCTION__ << endl;
}
private:
int m_id;
};
void test()
{
Test t1(1);
Test t2(2);
}
int main()
{
test();
return 0;
}
结果:
结论:
- 先创建的对象先构造,后创建的对象后构造
- 先创建的对象后析构,后创建的对象先析构
这个原因和函数调用内存有关,函数调用是压栈和出栈的过程,如果就想弄清楚,请看计算机系统相关的书籍,如:csapp
析构函数调用时间
- 在该对象生命周期结束后调用
构造/析构函数用途展示
构造函数:可以用来初始化对象,而且不需要显式调用,方便,快捷
析构函数:可以用来释放对象, 一次写好,没有后顾之忧(如:经常忘记delete、free)
class Man
{
public:
Man()
{
age = 18;
name = new char[20]{0};
strcpy(name,"maye");
}
~Man()
{
if(name!=nullptr)
{
delete[] name;
name = nullptr;
}
}
void print()
{
cout<<age<<" "<<name<<endl;
}
private:
int age;
char* name;
}
这样就可以避免自己忘记释放内存的情况了。
构造函数分类
构造函数是可以重载的,根据参数类型和作用可以分为以下几类:
无参构造函数
- 直接创建对象即可自动调用
Test te;
注意:不要在对象后面加(),无参构造函数不能显式调用
有参构造函数
-
有三种调用方法
//1,括号法 Test t1(20,"cc"); t1.print(); //2,赋值符号 Test t2 = {18,"wy"}; t2.print(); //3,匿名对象 Test t3 = Test(90,"wy"); t3.print(); //注意: Test tt; //error:类Test不存在默认构造函数,因为自己定义了构造函数 //** 匿名对象如果没有值来接收,那么就会被立即释放 ** Int(2, 3); //会立即释放 Int f = Int(2,3); //就不会立即释放
如果没有写有参构造函数,那么C++编译器会自动帮我们生成一个无参构造函数,如果写了有参构造函数,那么就不会帮我们生成了,必须自己写一个无惨构造函数,才能直接定义对象。
拷贝构造函数(赋值构造)
-
用一个对象去初始化另一个对象时(函数传参也会拷贝),需要拷贝构造(如果自己没有写,编译器会自动帮我们生成)
Test t(1,"2"); //1,赋值符号 Test t1 =t; //2,参数方法 Test t2(t); t2 = t1; //这个调用的是赋值运算符重载函数
-
注意:定义之后进行赋值不会调用拷贝构造函数,而是调用赋值函数,这是运算符重载,这个涉及到运算符重载的知识,这个我们稍后讲解,注意:拷贝构造与运算符重载很容易搞混
移动构造函数
- 移动构造函数数用来实现移动语义,转移对象之间的资源(如果自己没有写,编译器会自动帮我们生成),调用std::move()
// 定义一个对象
Test t1("wy", 18);
Test t2(std::move(t1)); //移动构造,这个时候对象t1所有权都转移给了t2,t1没有了资源,这样提高了资源的利用率
移动std::move()这个东西,我感觉很神奇,没有结合实践,感觉就这么回事,但是一结合实际,就会发现他特别伟大,特别好用!!!!
深拷贝和浅拷贝
首先,明确一点深拷贝和浅拷贝是针对类里面有指针的对象的,因为基本数据类型在进行赋值操作时(也就是拷贝)是直接将值赋给了新的变量,也就是该变量是原变量的一个副本,这个时候你修改两者中的任何一个的值都不会影响另一个,而对于对象来说在进行浅拷贝时,只是将对象的指针复制了一份,也就内存地址,即两个不同的对象里面的指针指向了同一个内存地址,那么在改变任一个对象的指针指向的内存的值时,都是该变这个内存地址的所存储的值,所以两个变量的值都会改变。
简单来说,当数据成员中有指针时,必须要用深拷贝。
- 浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址。
- 使用浅拷贝,释放内存的时候可能会出现重复释放同一块内存空间的错误。
- 深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存。
- 使用深拷贝下,释放内存的时候不会因为出现重复释放同一个内存的错误。
注意
- C++类中默认提供的拷贝构造函数,是浅拷贝的
- 要想实现深拷贝,必须自己手动实现拷贝构造函数
//自己实现深拷贝
TString(const TString& other) //普通:右值引用
{
if(&other != this) { // 不是自己拷贝自己
m_size = other.m_size;
m_str = new cahr[m_size + 1];
strcay(m_str,other.m_str);
}
}
int mian()
{
TString other = TString hello; //hello 为TString的一个实例化对象
}
构造函数的初始化参数列表
初始化参数列表
当我们再构造函数进行赋值成员变量的时候,可以有以下两种方法:
class Student
{
public:
// 方法一
Student(std::string uid, std::string name, int age)
{
m_uid = uid;
m_name = name;
m_age = age;
}
// 方法二,推荐:参数列表方法,不同变量之间用 ‘,’ 隔开
Student(std::string uid, std::string name, int age)
:m_uid(uid),
m_name(name),
m_age(age)
{
}
private:
std:string m_uid;
std::string m_name;
int m_age;
}
两种方法都可,但是我比较喜欢第二种。
类中类如何构造
类的组合:组合(有时候叫聚合)是将一个对象放到另一个对象里)。它是一种 has-a 的关系。
简单来说,就是一个类的对象作为另一个类的成员,这就叫做类的组合。
那这个时候这么对每一个对象值赋值呢?
假设我们再一个类B中创建了一个类A作为成员变量,而且A类中成员变量中,它只有一个带参数的构造函数,没有默认构造函数。这时要对这个类成员进行初始化,就必须调用这个类成员的带参数的构造函数
class A
{
public:
A(int a)
{
int m_a = a;
}
private:
int m_a;
}
// 定义类B
class B
{
public:
B(int b, int a)
:a1(a),
m_b1 = b
{
}
private:
int m_b1;
A a1; // 创建A对象
}
本类和对象成都需要执行构造函数,那么谁先执行呢?有什么样的顺序呢?
- 先指针被组合对象的构造函数,如果组合对象有多个,按照定义顺序,而不是按照初始化列表的顺序!
- 析构和构造顺序相反,这个再上面将构造和析构函数有讲解,如果大家忘了,可以回去看一下哦🤠🤠🤠