如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?
并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
class Date {};
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
构造函数是C++中的一个特殊函数,用于初始化类对象的数据成员,为对象分配内存并完成一些初始化工作。一个类可以有多个构造函数,但必须满足函数名相同、参数列表不同的条件,称为函数重载。
构造函数有以下特点:
(1)构造函数的函数名与类名相同,并且不需要返回类型的声明,在函数体中也不需要指定 return 语句。
(2)构造函数可以具有参数,用于传递初始值给对象的数据成员。
(3)构造函数可以进行重载,支持多个构造函数的存在。
(4)如果一个类没有定义自己的构造函数,编译器会自动生成一个默认的构造函数,该函数不带任何参数并且什么也不做,它会自动初始化类的成员变量并分配内存。
(5) 构造函数可以使用初始化列表进行初始化,这种方式可以提高效率。初始化列表是用冒号:跟在构造函数名后的成员初始化语句,以逗号隔开数据成员的名称和初始值。初始化列表的执行顺序与成员在类中声明的顺序一致。
(6) 如果一个类需要在离开作用域时,自动释放在堆内存上分配的资源,必须定义类的析构函数(Destructor)。
总结:构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
例子一(Person类):
class Person {
public:
string name;
int age;
Person(string n, int a) { // 带参数的构造函数
name = n;
age = a;
}
};
int main() {
Person p1("Alice", 20); // 使用构造函数创建对象
cout << p1.name << ", " << p1.age << endl; // 输出对象的成员变量
return 0;
}
我们定义了一个 Person 类,并创建了一个带有参数的构造函数来初始化对象的成员变量。
在 main() 函数中,我们使用类定义了一个 Person 对象,编译器在创建对象时会自动调用构造函数,初始化对象并输出该对象的成员变量值。
例子二(Date类):
class Date
{
public:
void Init(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;
};
int main()
{
Date d1;
d1.Init(2023, 5, 29);
d1.Print();
Date d2;
d2.Init(2023, 5, 29);
d2.Print();
return 0;
}
这段代码定义了一个名为 Date 的类。Date 类包含了三个私有的数据成员:_year(年份)、_month(月份)和 _day(日期)。该类提供了两个公有的成员函数: Init 和 Print。
Init 函数用于初始化 Date 对象的年、月、日信息,Print 函数则用于打印出 Date 对象的年、月、日信息。在主函数中,创建了两个 Date 对象,并通过 Init 函数初始化了其年、月、日的信息。
我们可以使用构造函数来代替Init函数的作用,使创建的数据成员初始化。这两段代码实现的功能完全一样。
#include
using namespace std;
class Date
{
public:
// 默认构造函数
Date() {
_year = 1949;
_month = 10;
_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;
};
int main()
{
// 使用构造函数初始化对象
Date d1(2023, 5, 29);
d1.Print();
Date d2(2023, 5, 29);
d2.Print();
return 0;
}
综上所述:构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
(1)函数名与类名相同。
(2)无返回值。
(3)对象实例化时编译器自动调用对应的构造函数。
(4)构造函数可以重载。
在函数 TestDate 中,分别通过无参构造函数和带参构造函数创建了三个 Date 类型的对象(d1、d2 和 d3)。其中,d1 使用了无参构造函数,d2 使用了带参构造函数,并传入年月日参数进行初始化。而 d3 也是通过无参构造函数创建的对象,但使用了错误的语法,即在对象后添加了一对空括号,则编译器会将其解析为函数声明,而不是对象的创建;正确的写法应该是 Date d3;。
class Date
{
public:
// 1.无参构造函数
Date()
{}
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
Date d1; // 调用无参构造函数
Date d2(2015, 1, 1); // 调用带参的构造函数
// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
// warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
Date d3();
}
(5)如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
在这段代码中,如果将 Date 类中的构造函数屏蔽掉,那么编译器将会自动生成一个默认的无参构造函数,此时主函数中的代码可以正常编译。但如果取消屏蔽,那么编译器将不再生成默认构造函数,主函数中的代码将会编译失败。
所以这个构造函数Date是错误的,因为在使用默认参数时,必须在函数声明或定义中为这些参数提供默认值,否则编译器会报错。
class Date
{
public:
/*
// 如果用户显式定义了构造函数,编译器将不再生成
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;
};
int main()
{
// 将Date类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
// 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生
成
// 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用
Date d1;
return 0;
}
(6)C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int / char …,自定义类型就是我们使用 class / struct /union 等自己定义的类型,编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。
Date 类包含了三个整型类型的基本数据成员 _year、_month、_day 和一个 Time 类型的私有数据成员 _t,其中 Time 类由于没有显式定义构造函数,因此会有一个默认的无参构造函数。
在主函数中,创建对象 d 时将调用 Date 类的默认构造函数,并在其中对其基本数据成员 _year、_month、_day 进行了初始化,Time 类型的成员 _t 也会调用其默认构造函数,将 _hour、 _minute、 _second 分别初始化为 0。
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
(7)无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
在这段代码中,Date 类显式声明了两个构造函数——其中一个是默认构造函数,另一个是带有三个参数的构造函数。在 Test() 函数中,没有提供任何参数来创建 Date 类型的对象 d1,因此将会使用默认构造函数进行初始化。
所有这个测试函数能够通过编译,且会成功创建一个 Date 类型的对象 d1,该对象的年月日属性分别为默认值 1900 年 1 月 1 日。
class Date
{
public:
Date()
{
_year = 1900;
_month = 1;
_day = 1;
}
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1;
}
在 C++ 中,析构函数(Destructor)是一种特殊的函数,其名称与类名称相同,但在名称前面加上一个波浪号(~)。析构函数与构造函数一样,也是一种特殊的成员函数,但它是在对象生命周期结束时自动调用的,并且只能有一个析构函数,且不能带有参数。
析构函数通常用于清理对象所占用的资源,比如释放动态申请的内存、关闭打开的文件等。具体来说,析构函数的工作包括释放内存或资源、删除临时文件、清理诸如打开的文件和数据库连接等。
析构函数有一下特点:
(1)在析构函数中,一般要释放对象所使用的内存或资源,防止内存泄漏。
(2)在堆上分配内存的对象,必须在析构函数中释放。
(3)调用析构函数的顺序与调用构造函数的顺序相反,即先析构派生类对象,再析构基类对象。
(4) 如果一个类有成员变量是指针类型或者是其他类的对象,需要在析构函数中先释放这些成员变量占用的内存,然后再释放自己的内存。
(5) 如果一个类没有显示地声明析构函数,则编译器会为该类生成一个默认的析构函数。默认的析构函数什么也不做。
总结:析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
简单的析构函数使用例子:
#include
using namespace std;
class A
{
public:
A()
{
cout << "A constructor" << endl;
}
~A()
{
cout << "A destructor" << endl;
}
};
int main()
{
A a;
return 0;
}
析构函数是特殊的成员函数,其特征如下:
(1)析构函数名是在类名前加上字符 ~。
(2)无参数无返回值类型。
(3)一个类只能有一个析构函数。 若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。
(4)对象生命周期结束时,C++编译系统系统自动调用析构函数。
以栈的实现为例:
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 其他方法...
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
void TestStack()
{
Stack s;
s.Push(1);
s.Push(2);
}
(5)编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month, _day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。
但是:main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数。
注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数。
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
//程序运行结束后输出:~Time()
(6)如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
这些就是C++中类和对象中构造函数和析构函数的简单介绍了
如有错误❌望指正,最后祝大家学习进步✊天天开心✨