类和对象
结构化程序设计
C语言使用结构化程序设计:
程序 = 数据结构 + 算法
程序由全局变量以及众多相互调用的函数组成。
算法以函数的形式实现,用于对数据结构进行操作。
结构化程序设计的不足:
结构化程序设计中,函数和其所操作的数据结构,没有直观的联系。
随着程序规模的增加,程序逐渐难以理解,很难一下子看出来:
某个数据结构到底有哪些函数可以对它进行操作?
某个函数到底是用来操作哪些数据结构的?
任何两个函数之间存在怎样的调用关系?
结构化程序设计没有“封装”和“隐藏”的概念。要访问某个数据结构中的某个变量,就可以直接访问,那么当该变量的定义有改动的时候,就要把所有访问该变量的语句找出来修改,十分不利于程序的维护、扩充。
难以查错,当某个数据结构的值不正确时,难以找
出到底是那个函数导致的。
重用:在编写某个程序时,发现其需要的某项功能,在现有的某个程序里已经有了相同或类似的实现,那么自然希望能够将那部分代码抽取出来,在新程序中使用。
在结构化程序设计中,随着程序规模的增大,由于程序大量函数、变量之间的关系错综复杂,要抽取这部分代码,会变得十分困难。
总之,结构化的程序,在规模庞大时,会变得难以理解,难以扩充(增加新功能),难以查错,难以重用。
软件业的目标是更快、更正确、更经济地建立软件。
• 如何更高效地实现函数的复用?
• 如何更清晰的实现变量和函数的关系?使得程序更清晰更易于修改和维护。
面向对象程序设计和面向过程程序设计的对比
下面分别给出一个简单示例展示面向对象程序设计和面向过程程序设计之间的区别:
面向对象程序设计示例(C++)
#include <iostream>
using namespace std;
class Rectangle // 定义矩形类
{
public:
double width; // 矩形宽度
double height; // 矩形高度
// 计算矩形面积
double area() { return width * height; }
// 输出矩形属性信息
void printInfo()
{
cout << "Width: " << width << endl;
cout << "Height: " << height << endl;
cout << "Area: " << area() << endl;
}
};
int main()
{
Rectangle r1; // 创建一个矩形对象
r1.width = 2.5; // 设置矩形宽度
r1.height = 3.7; // 设置矩形高度
r1.printInfo(); // 输出矩形属性信息
return 0;
}
在上述代码中,我们定义了一个Rectangle
类,包括矩形的属性(宽、高)以及行为(计算面积、输出信息)。然后在主函数中,创建了一个矩形对象,并通过其成员函数实现对矩形的操作。
面向过程程序设计示例(C语言)
#include <stdio.h>
// 计算矩形面积
double area(double width, double height)
{
return width * height;
}
// 输出矩形信息
void printInfo(double width, double height)
{
printf("Width: %.2f\n", width);
printf("Height: %.2f\n", height);
printf("Area: %.2f\n", area(width, height));
}
int main()
{
double w = 2.5; // 矩形宽度
double h = 3.7; // 矩形高度
printInfo(w, h); // 输出矩形属性信息
return 0;
}
在上述代码中,我们定义了area
函数和printInfo
函数来计算矩形的面积和输出矩形的属性信息。然后在主函数中,通过调用这些函数来实现对矩形的操作。
从上述两个示例可以看出,面向对象程序设计注重对象的封装、抽象和继承等特性,代码清晰、易读、易于修改,对于大型程序的开发而言具有较强的优势。面向过程程序设计则更加强调算法的设计和流程控制,并希望通过简单、清晰的代码来完成某些重复性工作。
面向对象的程序设计
面向对象的程序设计方法,能够较好解决上述问题。
面向对象的程序 = 类 + 类 + …+ 类
设计程序的过程,就是设计类的过程。
面向对象的程序设计方法:
将某类客观事物共同特点(属性)归纳出来,形成一个数据
结构(可以用多个变量描述事物的属性);将这类事物所能进行的行为也归纳出来,形成一个个函数,这些函数可以用来操作数据结构(这一步叫“抽象”)。然后,通过某种语法形式,将数据结构和操作该数据结构的函数“捆绑”在一起,形成一个“类”,从而使得数据结构和操作该数据结构的算法呈现出显而易见的紧密关系,这就是“封装”。
面向对象的程序设计具有“抽象”,“封装”“继承”“多态”四个基本特点。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5XOyg5g8-1685970170687)(2023-06-05-19-35-56.png)]
类和对象
在C++中,类(Class)是一种面向对象的概念,它描述了一个包含数据和方法(函数)的抽象实体,用来定义某个对象的属性和行为。类只是模板或蓝图,在创建对象时依据其定义,用于声明一个具有特定属性和功能的新数据类型。
而对象(Object)则是通过实例化一个类(可以理解为从类中生成一个具体的实例)而得到的一个真实存在的事物,拥有类所描述的属性和行为。通过操作对象的属性和行为,我们可以完成各种任务和操作。
例如,我们可以定义一个名为Rectangle
的类来描述矩形,如下所示:
class Rectangle
{
public:
double width;
double height;
double area()
{
return width * height;
}
};
该类中包含属性width
和height
,表示矩形的长和宽,并定义了一个成员函数area()
用于计算矩形的面积。现在我们可以通过实例化这个类来创建一个真正的矩形对象,如下所示:
Rectangle r; // 创建一个矩形对象
r.width = 2.5; // 设置矩形的宽度
r.height = 3.7; // 设置矩形的高度
double a = r.area(); // 调用矩形的成员函数计算矩形的面积
这里的r
就是一个矩形对象,它包含了类Rectangle
中定义的属性和行为(成员函数),我们可以通过直接操作这些属性来进行计算,而不必关心具体的实现方式。
C++中的类和对象提供了一种抽象和封装的机制,帮助程序员更好地管理和组织代码,并以更高效、更安全的方式进行编程和设计。
对象的内存分配
和结构变量一样,对象所占用的内存空间的大小,等于所有成员变量的大小之和。
对于上面的CRectangle类,sizeof(CRectangle) = 8
每个对象各有自己的存储空间。一个对象的某个成员变量被改变了,不会影响到另一个对象。
对象间的运算
和结构变量一样,对象之间可以用 “=”进行赋值,但是不能用 “==”,“!=”,“>”,“<”“>=”“<=”进行比较,除非这些运算符经过了“重载”。
用法1:对象名.成员名
CRectangle r1,r2;
r1.w = 5;
r2.Init(5,4); //Init函数作用在 r2 上,即Init函数执行期间访问的
//w 和 h是属于 r2 这个对象的, 执行r2.Init 不会影响到 r1。
用法2. 指针->成员名
CRectangle r1,r2;
CRectangle * p1 = & r1;
CRectangle * p2 = & r2;
p1->w = 5;
p2->Init(5,4); //Init作用在p2指向的对象上
用法3:引用名.成员名
CRectangle r2;
CRectangle & rr = r2;
rr.w = 5;
rr.Init(5,4); //rr的值变了,r2的值也变
void PrintRectangle(CRectangle & r)
{ cout << r.Area() << ","<< r.Perimeter(); }
CRectangle r3;
r3.Init(5,4);
PrintRectangle(r3);
类成员的可访问范围
在类的定义中,用下列访问范围关键字来说明类成员可被访问的范围:
– private: 私有成员,只能在成员函数内访问
– public : 公有成员,可以在任何地方访问
– protected: 保护成员,以后再说
以上三种关键字出现的次数和先后次序都没有限制。
定义一个类
class className {
private:
私有属性和函数//说明类成员的可访问范围
public:
公有属性和函数//说明类成员的可访问范围
protected:
保护属性和函数//说明类成员的可访问范围
};
如果某个成员前面没有上述关键字,则缺省地被认为
是私有成员。
class Man {
int nAge; //私有成员
char szName[20]; // 私有成员
public:
void SetName(char * szName){
strcpy( Man::szName,szName);
}
};
在类的成员函数内部,能够访问:当前对象的全部属性、函数;同类其它对象的全部属性、函数。
在类的成员函数以外的地方,只能够访问该类对象的公有成员
class CEmployee {
private:
char szName[30]; //名字
public :
int salary; //工资
void setName(char * name);
void getName(char * name);
void averageSalary(CEmployee e1,CEmployee e2);
};
void CEmployee::setName( char * name) {
strcpy( szName, name); //ok
}
void CEmployee::getName( char * name) {
strcpy( name,szName); //ok
}
void CEmployee::averageSalary(CEmployee e1,
CEmployee e2){
cout << e1.szName; //ok,访问同类其他对象私有成员
salary = (e1.salary + e2.salary )/2;
}
int main()
{
CEmployee e;
strcpy(e.szName,"Tom1234567889"); //编译错,不能访
问私有成员
e.setName( "Tom"); // ok
e.salary = 5000; //ok
return 0;
}
int main()
{
CEmployee e;
strcpy(e.szName,"Tom1234567889"); //编译错,不能访
问私有成员
e.setName( "Tom"); // ok
e.salary = 5000; //ok
return 0;
设置私有成员的机制,叫“隐藏”
“隐藏”的目的是强制对成员变量的访问一定要通过成员函数进行,那么以后成员变量的类型等属性修改后,只需要更改成员函数即可。否则,所有直接访问成员变量的语句都需要修改。
如果将上面的程序移植到内存空间紧张的手持设备上,希望szName 改为 char szName[5],若szName不是私有,那么就要找出所有类似
strcpy(e.szName,“Tom1234567889”);
这样的语句进行修改,以防止数组越界。这样做很麻烦。
如果将szName变为私有,那么程序中就不可能出现(除非在类的
内部)strcpy(e.szName,“Tom1234567889”);这样的语句,所有对 szName的访问都是通过成员函数来进行,比如:
e.setName( “Tom12345678909887”);
那么,就算szName改短了,上面的语句也不需要找出来修改,只要改 setName成员函数,在里面确保不越界就可以了。
用struct定义类
struct CEmployee {
char szName[30]; //公有!!
public :
int salary; //工资
void setName(char * name);
void getName(char * name);
void averageSalary(CEmployee
e1,CEmployee e2);
};
和用"class"的唯一区别,就是未说明是公有还是私有的成员,就是公有
成员函数的重载及参数缺省
成员函数的重载(Overloading)指的是在同一个类中定义多个名称相同但参数个数或参数类型不同的成员函数,以实现类似的功能但具有不同的行为。重载可以极大提高代码的复用性和可读性,在需要使用同一函数名但行为却略有不同的情况下,使用重载能够让代码更为简洁。
重载的方式具体有两种:
- 同名不同参:函数名称相同,但是参数个数或类型不同,如下所示:
class Rectangle
{
public:
double width;
double height;
double area()
{
return width * height;
}
int area(int times)
{
return width * height * times;
}
};
上述例子定义了两个area
方法,其功能都是计算矩形面积。在第一个函数中,该方法不接受任何参数,返回浮点数类型的计算结果;而在第二个函数中,该方法接受一个整型参数,并将浮点数类型的面积值乘以这个参数,最后返回整型类型的计算结果。这样在调用时,可以根据不同的需求选择不同的方法来处理数据:
Rectangle r;
r.width = 2.5;
r.height = 3.7;
double a = r.area(); // 调用第一个area方法
int b = r.area(2); // 调用第二个area方法
- 同名同参但类型不同:函数名称和参数完全相同,但是返回值类型不同。例如,可以有一个成员函数和一个友元函数都名为
operator+()
, 其形参和行为相同,只是前者的调用方式限定在该类的对象上。
成员函数也支持参数缺省(Default Arguments)的语法,允许在定义成员函数时声明某个或某些参数的默认值,而在函数调用时如果没有传递对应的参数,则使用默认值,如下所示:
class Rectangle
{
public:
double width;
double height;
double area(double rate = 1.0)
{
return width * height * rate;
}
};
上述代码中, double rate = 1.0
声明了一个默认参数,当调用该函数时如果没有指定rate值,则默认为1.0。这个特性广泛用于提高重载函数的可读性,增加使用方便性。
总之,通过使用重载和参数缺省,我们可以面向对象设计中实现更丰富、灵活和易用的编程风格来应对不同的运算需求。
使用缺省参数要注意避免有函数重载时的二义性
class Location {
private :
int x, y;
public:
void init( int x =0, int y = 0 );
void valueX( int val = 0) { x = val; }
int valueX() { return x; }
};
Location A;
A.valueX(); //错误,编译器无法判断调用哪个valueX
构造函数(constructor)
基本概念
成员函数的一种
名字与类名相同,可以有参数,不能有返回值(void也不行)
作用是对对象进行初始化,如给成员变量赋初值
如果定义类时没写构造函数,则编译器生成一个默认的无参数的构造函数
•默认构造函数无参数,不做任何操作
如果定义了构造函数,则编译器不生成默认的无参数的构造函数对象生成时构造函数自动被调用。对象一旦生成,就再也不能在其上执行构造函数一个类可以有多个构造函数
为什么需要构造函数:
- 构造函数执行必要的初始化工作,有了构造函数,就不必专门再写初始化函数,也不用担心忘记调用初始化函数。
- 有时对象没被初始化就使用,会导致程序出错。
class Complex {
private:
double real, imag;
public:
void Set( double r, double i);
}; //编译器自动生成默认构造函数
Complex c1; //默认构造函数被调用
Complex * pc = new Complex; //默认构造函数被调用
class Complex {
private :
double real, imag;
public:
Complex( double r, double i = 0);
};
Complex::Complex( double r, double i) {
real = r; imag = i;
}
Complex c1; // error, 缺少构造函数的参数
Complex * pc = new Complex; // error, 没有参数
Complex c1(2); // OK
Complex c1(2,4), c2(3,5);
Complex * pc = new Complex(3,4);
可以有多个构造函数,参数个数或类型不同
class Complex {
private :
double real, imag;
public:
void Set( double r, double i );
Complex(double r, double i );
Complex (double r );
Complex (Complex c1, Complex c2);
};
Complex::Complex(double r, double i)
{
real = r; imag = i;
}
Complex::Complex(double r)
{
real = r; imag = 0;
}
Complex::Complex (Complex c1, Complex c2);
{
real = c1.real+c2.real;
imag = c1.imag+c2.imag;
}
Complex c1(3) , c2 (1,0), c3(c1,c2);
// c1 = {3, 0}, c2 = {1, 0}, c3 = {4, 0};
构造函数最好是public的,private构造函数
不能直接用来初始化对象
class CSample{
private:
CSample() {
}
};
int main(){
CSample Obj; //err. 唯一构造函数是private
return 0;
}
在C++中,构造函数(Constructor)是一种特殊的成员函数,它用于在对象创建时初始化类的成员变量。通常,每个类都至少有一个构造函数。
构造函数的名称必须与类的名称相同,没有返回类型(void、int等),并且不能手动调用。构造函数的作用是在创建对象时为成员变量赋初值,如下所示:
class Rectangle
{
public:
double width;
double height;
// 构造函数
Rectangle()
{
width = 0;
height = 0;
}
};
上述代码定义了一个Rectangle
类,并在其中定义了一个构造函数,该构造函数将宽度和高度初始化为空值。
构造函数在对象被创建时自动执行,可以根据不同需求重载多个不同的构造函数。比如,在构造函数中使用参数列表为成员变量赋值:
class Rectangle
{
public:
double width;
double height;
// 构造函数 #1
Rectangle(double w, double h)
{
width = w;
height = h;
}
// 构造函数 #2
Rectangle()
{
width = 0;
height = 0;
}
};
上述代码中定义了两个构造函数,第一个构造函数用来传入实际的宽度和高度进行初始化,而第二个构造函数不需要任何参数,则按照预设值将其初始化为0。
通过构造函数,在创建对象时就可以有效地进行成员变量的初始化,提高了代码的效率和可读性。需要注意的是,在一个类中只能有一个析构函数(Destructor),它与构造函数类似但在对象被销毁时启动执行。
另外需要注意的是,C++中的构造函数也可以通过初始化列表(Initialization List)的方式来对成员变量进行初始化,这种方式比在构造函数体中赋值效率更高,并且允许直接初始化常量和引用类型的成员变量。如下所示:
class Rectangle
{
public:
double width;
double height;
// 构造函数 #1
Rectangle(double w, double h): width(w), height(h)
{
// 无需将参数分别复制到成员变量中
}
// 构造函数 #2
Rectangle() : width(0), height(0)
{
// 直接将成员变量初始化为0
}
};
最后还需要提出的是,如果一个类没有定义构造函数,则编译器将自动提供一个默认的构造函数,其中的成员变量都将按照其数据类型的默认规则进行初始化,如int为0、string为空等。如果需要完全掌握类的初始化过程,建议手动实现构造函数以确保代码的可靠性和安全性。
//构造函数在数组中的使用
在C++中,我们可以通过定义构造函数来自定义对象的创建和初始化过程。而当需要创建一个对象数组时,同样也可以使用构造函数来对每个对象进行初始化。
在创建对象数组时,编译器会首先调用默认的构造函数来初始化其中的所有对象。如果类定义了自己的构造函数,则编译器会使用这个构造函数来进行初始化。例如,下面的代码定义了一个Rectangle
类,并在该类中添加了一个构造函数:
class Rectangle
{
public:
double width;
double height;
// 构造函数
Rectangle(double w, double h)
{
width = w;
height = h;
}
};
可以按常规方式声明和实例化一个对象:
Rectangle r(3.5, 4.5);
但是如果要创建多个矩形怎么办呢?我们可以使用类似于下面示例的数组语法来进行初始化:
Rectangle rects[] = {Rectangle(1, 2), Rectangle(2, 3), Rectangle(3, 4)};
上述代码中,我们利用花括号{}列表形式来对数组元素进行初始化,其中每个元素都使用了 Rectangle
的构造函数来创建并初始化它们自己的成员变量width
和height
。
需要注意,当使用构造函数创建对象数组时,数组中的每个元素都是独立的实例,它们之间没有任何关联。因此,在不同位置修改其中任意的元素都不会影响其他元素,也就是说它们是彼此独立的。
总之,通过构造函数我们可以在创建对象时对其成员变量进行初始化,并利用数组语法来创建多个并存的对象。这种方法非常灵活,方便快捷,在实际开发中具有很高的应用价值。
除了以上提到的花括号{}列表初始化方法,还可以使用循环来初始化对象数组中的元素。例如:
Rectangle rects[3]; // 创建一个长度为3的矩形数组
for (int i = 0; i < 3; ++i)
{
rects[i] = Rectangle(i+1, i+2); // 使用构造函数创建并赋值给每个数组元素
}
上述代码中,我们首先创建了一个长度为3的Rectangle
类型数组,并且没有指定初始值,然后使用循环遍历数组的每个元素,使用构造函数进行初始化。需要注意的是,这种方法只有在数组已经被定义时才能使用,因为在C++中数组一旦被定义就不能改变其大小。
总之,使用构造函数初始化对象数组的方法非常灵活多样,开发者可以自由选择合适的方法来实现自己的需求。同时,也需要注意在对象数组的使用中要保证构造函数的正确性和安全性,以避免潜在的问题。
除了在对象数组中使用构造函数进行初始化外,有时候我们还需要手动调用析构函数来释放对象占用的资源。在C++中,通过使用delete操作符可以显式地调用对象的析构函数,并将其从内存中销毁。
例如,在下面这个例子中,我们定义了一个类MyClass
,并在其中添加了构造函数和析构函数:
class MyClass
{
public:
MyClass() { /* 构造函数代码 */ }
~MyClass() { /* 析构函数代码 */ }
};
此时,如果我们想要手动销毁某些对象实例,可以使用delete
操作符来调用它们的析构函数,如下所示:
MyClass* obj = new MyClass(); // 创建一个MyClass类型对象
// 在不需要obj对象时,手动释放该对象
delete obj; // 调用析构函数并将对象从内存中销毁
上述代码中,我们利用关键字new
来创建了一个对象实例,并将其保存到指向该对象的指针obj
中。然后,在不再需要该对象时,我们可以使用delete
操作符手动释放并销毁该对象,从而避免内存泄露和资源浪费问题。
需要注意的是,在调用delete操作符时,如果删除的对象是通过数组元素创建的,则必须使用delete[]
操作符来销毁该数组,如下所示:
MyClass* objs = new MyClass[10]; // 创建一个包含10个对象的数组
// 在不需要objs数组时,手动释放该数组
delete[] objs; // 调用析构函数并将数组从内存中销毁
总之,在C++中使用构造函数和析构函数,可以在对象创建和销毁时对其进行一系列操作,例如对成员变量进行初始化和资源释放等,具有极高的可定制性和实用价值。但同时也要注意对构造函数和析构函数的正确使用和合理管理,以确保代码效率和正确性。
//复制构造函数 copy constructor
基本概念
只有一个参数,即对同类对象的引用。
形如 X::X( X& )或X::X(const X &), 二者选一后者能以常量对象作为参数
如果没有定义复制构造函数,那么编译器生成默认复制构造函数。默认的复制构造函数完成复制功能。
复制构造函数是一个特殊的构造函数,用于创建一个新对象,该对象与另一个已经存在的同类对象具有相同的值。通常在需要创建一个与已有对象值相同的新对象时使用。下面是一个C++语言的示例代码,其中定义了一个名为Person的类,包含成员变量name和age,并实现了一个复制构造函数。
#include <iostream>
#include <string>
using namespace std;
class Person {
public:
string name;
int age;
Person(string n, int a) : name(n), age(a) {} // 构造函数
Person(const Person &p) { // 复制构造函数
name = p.name;
age = p.age;
}
};
int main() {
Person p1("Alice", 20);
Person p2 = p1; // 使用复制构造函数创建p2对象,其值与p1相等
cout << p1.name << " " << p1.age << endl;
cout << p2.name << " " << p2.age << endl;
return 0;
}
在上述示例代码中,我们定义了一个Person类,它包含两个成员变量:name和age。在构造函数中初始化这两个成员变量。另外,还实现了一个复制构造函数,在创建新的Person类对象时,将原对象的属性进行拷贝并赋给新对象,从而使新对象与原对象的值相等。
在主函数中,首先创建了一个Person类型的对象“p1”,然后通过使用复制构造函数创建了一个新对象“p2”。最后,分别输出两个对象的姓名和年龄。可以看到,p1和p2的值是相等的。
总之,复制构造函数在C++语言中是一个很常用的函数,通过它我们可以将已有对象的值复制并赋给一个新的对象。
复制构造函数有以下几个特点:
- 它是一个特殊的构造函数,带有唯一的参数,即对同类的引用。
- 当使用同类的对象来初始化一个新的对象时,复制构造函数会被自动调用。例如,如上述示例中的语句“Person p2 = p1;”。
- 复制构造函数的作用是实现对象的浅拷贝或深拷贝。在进行复制操作时,可以根据需要选择对成员变量进行赋值,从而使得新对象与原对象具有相同的值。
- 如果没有显式地定义复制构造函数,编译器也会自动生成一个默认的复制构造函数,但该函数只是简单地将原始对象的所有成员变量复制到新对象中,不能满足所有情况的需求,因此通常我们需要根据特定场景自己定义复制构造函数。
需要注意的是,由于复制构造函数经常用到,所以如果程序中存在指向动态内存的指针或者引用,那么在定义复制构造函数时要非常小心。复制构造函数中的逻辑运算应该保证复制后的对象和原来的对象彼此独立,而且同样避免出现野指针等错误。
通常情况下,当类中包含了动态分配的内存时,需要重载复制构造函数以实现正确的深度复制。下面给出一个示例代码:
class Person {
public:
string *name; // 定义指向string类型的指针
int age;
Person(string n, int a) : age(a) { // 构造函数
name = new string(n); // 动态分配内存并初始化
}
Person(const Person &p) : age(p.age) { // 复制构造函数
name = new string(*(p.name)); // 深拷贝指向string的指针
}
~Person() { // 析构函数
delete name; // 释放动态分配的内存
}
};
在上述示例中,我们更改了Person
类的实现方式,将其中的name
变量从string
类型变为了指向string
类型的指针,并重载了复制构造函数与析构函数。
在构造函数中,我们使用了动态内存分配语句new string(n)
,这意味着我们为name
变量分配了一块新的内存,并将它的指针赋值给了name
。同时,在析构函数中通过调用delete name
来释放动态分配的内存。
在复制构造函数中,我们为新对象分配了一块新的内存,并将原对象指针所指向的内容复制到新的内存中。这样,我们实现了对原对象和新对象内存空间的分离管理。
总之,正确地重载复制构造函数能够避免由于复制操作引起的一些问题,如野指针、内存泄漏等等。因此,在定义类的时候,如果有必要使用动态内存,就必须同时重新制定析构函数与复制构造函数来进行动态内存管理。
复制构造函数一般用于以下场景:
-
对象的初始化:在创建对象时,可以通过拷贝已有对象来初始化新的对象。此时,编译器会自动调用复制构造函数。
-
对象的赋值:当把一个对象赋值给另一个对象时,也会调用复制构造函数。例如,
Person p1; Person p2 = p1;
。这样就会使用p1
对象的值进行初始化p2
对象。 -
以值传递方式传递对象:当函数参数是一个对象时,如果该参数被以值传递的方式传递,那么将会调用其复制构造函数。例如,在定义函数
void printPerson(Person p);
时,如果调用printPerson(p1)
,那么它就会调用复制构造函数进行参数传递。
需要注意的是,在上述三种情况下,会自动调用复制构造函数,从而创建一个与原始对象相同的新对象。但在实际业务中,由于各种原因,程序员有时也要手动调用复制构造函数,来产生一个新对象并初始化它的内容。
//常量引用参数的使用
void fun(CMyclass obj_ ) {
cout << "fun" << endl;
}
这样的函数,调用时生成形参会引发复制构造函数调用,开销比较大。
所以可以考虑使用 CMyclass & 引用类型作为参数。
如果希望确保实参的值在函数中不应被改变,那么可以加上const 关键字:
void fun(const CMyclass & obj) {
//函数中任何试图改变 obj值的语句都将是变成非法
}
类型转换构造函数
定义转换构造函数的目的是实现类型的自动转换。
只有一个参数,而且不是复制构造函数的构造函数,一般就可以看作是转换构造函数。
当需要的时候,编译系统会自动调用转换构造函数,建立一个无名的临时对象(或临时变量)。
类型转换构造函数是一种将一个对象从一种类型转换为另一种类型的方式。
在C++中,我们可以在类中定义类型转换构造函数,来实现将类的对象隐式地转换成其他类型。其中,类型转换构造函数也是一种特殊的构造函数,它允许直接将一种类型的对象转换为另一种类型的对象。
下面给出一个简单的示例:
#include<iostream>
using namespace std;
class Complex{
private:
double real,imag;
public:
Complex(int a):real(a), imag(0){ // 定义类型转换构造函数
cout << "Converted!" << endl;
}
};
int main(){
int num = 3;
Complex c1 = num; // 将整型数num转换为Complex类型,此处会自动调用类型转换构造函数
return 0;
}
在上述代码中,我们定义了一个名为Complex
的类,包含两个成员变量real
和imag
。在这个类的定义中,我们定义了一个类型转换构造函数,该构造函数只有一个参数,即一个整型数a
,它用于把整数转换成复数。
在主函数中,我们定义了一个整数类型的变量num
,并使用Complex c1 = num;
语句将它隐式转换为了Complex
类型的对象。运行程序后,可以看到输出信息“Converted!”,表示成功调用了类型转化构造函数。
总之,类型转换构造函数是一种可用于将一种类的对象隐式转化为另一种类型的构造函数。虽然可以很方便地将它们用作数据类型转换,但需要注意的是,它可能会导致程序出现潜在的问题(如精度误差等)。因此,在使用时必须小心谨慎。
//析构函数destructors
名字与类名相同,在前面加‘~’, 没有参数和返回值,一个类最多只能有一个析构函数。
析构函数对象消亡时即自动被调用。可以定义析构函数来在对象消亡前做善后工作,比如释放分配的空间等。
如果定义类时没写析构函数,则编译器生成缺省析构函数。缺省析构函数什么也不做。
如果定义了析构函数,则编译器不生成缺省析构函数。
析构函数是与构造函数相对应的一种特殊函数,主要用于在对象生命周期结束时清理对象占用的内存资源。
在C++中,每个类都可以定义自己的析构函数,它由一个波浪号~
和类名组成,并且不需要参数或返回值。通常情况下,需要清理动态分配的内存、关闭打开的文件、释放网络连接等等操作都可以在析构函数中实现。
在下面的示例代码中,我们定义了一个名为Person
的类,其中包含有两个成员变量name
和age
,我们在这个类中定义了一个简单的析构函数,用于在对象被销毁时输出一条消息:
#include<iostream>
using namespace std;
class Person {
public:
string name;
int age;
Person(string n, int a): name(n), age(a) { // 构造函数
cout << "Object created." << endl;
}
~Person(){ // 析构函数
cout << "Object destroyed." << endl;
}
};
int main() {
Person p1("张三", 20);
return 0;
}
在上述代码中,我们定义了一个Person
对象p1
,并初始化了它的属性(姓名和年龄)。在main()
函数结束后,程序运行完毕,也就是Person
对象p1
的生命周期结束时,会自动调用析构函数~Person()
来释放占用的内存资源。在这个析构函数中,我们简单地输出了一条消息“Object destroyed.”。
总之,析构函数是类的一个特殊函数,在对象生命周期结束时自动调用(或者手动调用),它的主要作用就是清理对象所占用的资源,例如动态分配的内存、打开的文件等。借助析构函数,我们能够更好地控制程序使用内存资源的方式,并避免出现内存泄漏等问题。
为什么需要析构函数
在C++中,我们经常要使用动态内存分配来创建对象,例如使用new
关键字来为一个对象分配内存空间。由于程序运行完毕后需要释放这部分动态分配的内存空间,因此我们需要使用delete
关键字来释放这些内存空间。
如果我们没有合理地释放这些内存,在程序执行一段时间之后,就会耗尽机器物理内存,导致系统崩溃或程序运行效率下降。因此,为了避免出现内存泄漏等问题,我们可以使用析构函数来释放所占用的内存资源。
此外,析构函数的另一个重要作用是帮助我们管理类中的各种数据结构和状态。通常我们可以使用析构函数来释放资源但需要注意的是,在大多数情况下,析构函数在对象被销毁时自动调用,程序员无需手动调用。
综上所述,析构函数是为了解决程序中动态内存申请和管理的问题而存在的,它是一种特殊的函数,当对象生命周期结束时自动调用,主要用于清理对象所占用的资源、还原对象的状态等。使用析构函数能够有效地管理内存资源,防止内存泄漏以及其它相关问题的发生。
析构函数和数组
在C++中,数组的生命周期也和其他对象一样;当程序执行到数组的作用域结束时,或者使用delete
运算符来释放动态分配的数组空间时,数组也要被销毁。因此,如果一个类定义了析构函数,则它可以管理动态数组,并在销毁对象时自动调用。
下面是一个简单的示例代码,展示了如何在类中使用析构函数来管理动态数组:
#include<iostream>
using namespace std;
class Array{
private:
int *arr;
int size;
public:
Array(int s):size(s) { // 构造函数,创建一个大小为s的数组
arr = new int[s];
}
~Array() { // 析构函数,释放数组所占用的内存
delete[] arr;
}
};
int main(){
Array a1(10); // 创建大小为10的数组对象
return 0;
}
在上述代码中,我们定义了一个名为Array
的类,其中包含一个指向int
类型的指针arr
,用于动态分配内存,并且定义了一个整型变量用于记录数组的大小。在类的构造函数Array(int s)
中,我们使用new
运算符为这个数组分配了s
个元素的内存,并保存指向其地址的指针。然后,在析构函数~Array()
中,我们使用delete[]
运算符来释放数组的内存空间。
在main()
函数中,我们创建一个大小为10
的数组对象a1
,用于展示如何在类中使用析构函数来管理动态数组。当程序执行结束时,自动调用Array
类的析构函数,进而自动释放动态分配的内存空间。
值得注意的是,在使用析构函数管理动态数组时需要小心一些细节问题,下面列出几点需要特别注意的事项:
-
析构函数应该使用
delete[]
运算符释放数组所占用的内存空间,否则可能会导致内存泄漏或程序崩溃。 -
如果在类中定义了其他指针类型的成员变量,例如
char*
型指针,则需要编写相应的析构函数来释放这些指针所指向的内存空间。 -
在使用动态数组时,应该进行异常处理,防止因为内存分配失败导致内存泄漏或程序崩溃。
-
除非有必要,否则不应该将动态数组作为类成员来使用,否则可能会破坏类的封装性和可移植性。如果必须使用动态数组作为类成员变量,则应该使用复制构造函数和赋值运算符来处理对象之间的拷贝和赋值操作。
总之,使用析构函数可以有效地管理动态数组并防止内存泄漏等问题,但需要程序员对其进行正确的使用与实现。
析构函数和运算符 delete
delete 运算导致析构函数调用。
Ctest * pTest;
pTest = new Ctest; //构造函数调用
、
pTest = new Ctest[3]; //构造函数调用3次
delete [] pTest; //析构函数调用3次
若new一个对象数组,那么用delete释放时应该写 []。否则只delete一个对象(调用一次析构函数)
析构函数在对象作为函数返回值返回后被调用
class CMyclass {
public:
~CMyclass() { cout << "destructor" << endl; }
};
CMyclass obj;
CMyclass fun(CMyclass sobj ) { //参数对象消亡也会导致析
//构函数被调用
return sobj; //函数调用返回时生成临时对象返回
}
int main(){
obj = fun(obj); //函数调用的返回值(临时对象)被
return 0; //用过后,该临时对象析构函数被调用
}
输出:
destructor
destructor
destructor
构造函数和析构函数什么时候被调用
构造函数和析构函数的调用时机如下:
构造函数:
- 在创建对象时自动被调用,即当我们定义一个类的对象,使用它的构造函数来初始化该对象时就会被自动调用。
- 构造函数还可以在派生类中显示调用基类的构造函数。这通常是通过在子类的成员初始化列表中调用它来实现的。
析构函数:
- 当一个对象生命到达其范围的尽头或者当对动态分配内存空间的对象应用
delete
运算符时,析构函数将被自动调用。 - 在一些情况下,析构函数也可以显式地调用来释放对象的资源。
例如,考虑下面这个小例子:
#include<iostream>
using namespace std;
class Person {
public:
string name;
int age;
Person() {
cout << "Constructor called!" << endl;
}
~Person() {
cout << "Destructor called!" << endl;
}
};
int main() {
Person p1; // 创建Person类型的对象p1,此时构造函数被调用
return 0;
}
在上述代码中,我们定义了一个名为Person
的类,并在其中定义了构造函数和析构函数。在main()
函数中,我们创建一个Person
类型的对象p1
,它的定义触发了构造函数的调用;在程序结束之前,Person
对象p1
的范围已经结束,同时达到了它的生命周期结束这一条件,触发了析构函数的调用。
总之,构造函数和析构函数都将在对象创建和销毁时分别被自动调用。对于不同类型的类,当创建或销毁实例时,C++编译器会自动调用相应的构造函数和析构函数来处理相关的操作。
需要注意的是,如果一个类包含静态成员变量,则必须显式地定义该静态成员变量的构造函数和析构函数。因为静态对象必须在程序开始执行之前被初始化,在程序结束时被销毁。例如:
#include<iostream>
using namespace std;
class MyClass {
public:
MyClass() {
cout << "Constructor called." << endl;
}
~MyClass() {
cout << "Destructor called." << endl;
}
};
class WithStaticMember{
public:
static MyClass sm; // 静态成员变量,必须显式定义其构造函数和析构函数
WithStaticMember() {
cout << "WithStaticMember constructor called." << endl;
}
~WithStaticMember() {
cout << "WithStaticMember destructor called." << endl;
}
};
MyClass WithStaticMember::sm; // 显式定义静态成员变量的构造函数和析构函数
int main() {
WithStaticMember wsm; // 创建对象wsm,此时调用相关构造函数
return 0;
}
在上述代码中,我们定义了一个名为WithStaticMember
的类,并在其中定义了包含一个静态成员变量sm
的类成员。由于静态成员变量必须在类外部进行初始化,所以我们需要在程序文件级别上定义其构造函数和析构函数。在main()
函数中,我们创建WithStaticMember
类的实例wsm
,此时相关构造函数自动调用;当程序结束时,对应的析构函数被自动调用。
总之,在包含静态成员变量的类中,必须显式定义其构造函数和析构函数以正确地初始化和销毁这些变量,否则会导致程序无法编译或运行异常。