当前位置: 首页 > news >正文

【C++】Class(1)

在这里插入图片描述

《C++程序设计基础教程》——刘厚泉,李政伟,二零一三年九月版,学习笔记


文章目录

  • 1、类的定义
    • 1.1、结构体和类
    • 1.2、基本概念
    • 1.3、成员函数的定义
    • 1.4、内联成员函数
  • 2、对象
    • 2.1、对象的定义
    • 2.2、成员访问
  • 3、构造函数
    • 3.1、构造函数的定义
    • 3.2、子对象与构造函数
    • 3.3、拷贝构造函数
  • 4、析构函数
    • 4.1、析构函数的定义
    • 4.2、构造函数和析构函数的调用顺序
    • 4.3、对象的动态建立与释放
  • 5、静态成员
    • 5.1、静态数据成员
    • 5.2、静态成员函数
    • 5.3、静态成员的访问
  • 6、应用实例
  • 附录——堆和栈

1、类的定义

类(Class)和对象(Object)是面向对象程序设计中的最重要的基本概念。

对象是类的实例,类与对象的关系相当于数据类型和变量之间的关系。

对象是能体现现实世界物体基本特征的抽象实体,反映在软件系统中就是一些属性和方法的封装体。

对象就是“数据+作用于数据上的操作(方法)”

1.1、结构体和类

在C++中,结构体(struct)和类(class)都是用户自定义的数据类型,用于将多个数据成员(变量)和成员函数(方法)组合在一起。尽管它们在许多方面是相似的,但存在一些关键区别,这些区别主要源于C++对它们的默认访问控制、继承方式以及成员函数的默认可见性等方面的处理。

区别

  • 默认访问权限:
    类(class):成员的默认访问权限是私有的(private),这意味着类的成员在类外部是不可访问的,除非显式地声明为 public 或protected。
    结构体(struct):成员的默认访问权限是公有的(public),这使得结构体的成员在定义后可以直接被外部访问。

  • 继承控制:
    类:默认继承模式是私有继承(private),即基类的公有成员和保护成员在派生类中默认会变成私有成员,除非明确声明继承模式。
    结构体:默认继承模式是公有继承(public),即基类的公有成员在派生结构体中仍然是公有的,保护成员仍然是保护的。

  • 设计意图和应用场景:
    类:通常用于定义复杂的数据抽象和行为封装,强调面向对象的编程理念,如封装、继承和多态。类更适合用于需要面向对象编程(Object-Oriented Programming,OOP)的场景,如支持多态、继承、虚函数等复杂的设计模式。
    结构体:通常用于定义较为简单的数据结构,重点在于数据的存储,而不涉及太多的行为。结构体更适合用于组织和存储简单数据,特别是数据结构和轻量级对象。

  • 语义上的差异:
    尽管在技术上,C++中的类和结构体在功能上几乎等价,但它们在语义上有细微的差别。类更倾向于表示带有行为的对象,而结构体则更多地被用作数据容器。

联系

  • 语法和功能上的相似性:
    在C++中,类和结构体在语法和功能上几乎完全一致。它们都可以包含数据成员和成员函数,用于定义用户自定义的数据类型。

  • 内存布局:
    类和结构体的内存布局都受内存对齐的影响,编译器会根据成员的类型对其进行适当的对齐,以优化访问性能。

  • 使用上的灵活性:
    在现代C++中,**结构体不再局限于传统C语言中的简单数据结构,它也能包含构造函数、析构函数和成员函数,甚至支持继承和多态。**因此,类和结构体的选择更多是基于设计意图和编程风格,而非功能上的限制。

总结

  • 类:适合用于创建具有复杂行为、封装性、继承关系和多态支持的对象。常用于设计复杂系统、管理资源和封装实现细节。

  • 结构体:更适合用于组织和存储简单数据,特别是在不需要行为封装的场景下。它们倾向于用在数据结构和轻量级对象中。

#include <iostream>
#include <string.h> // 用于 strcpy
using namespace std;


class CStudent
{
    private:
        string Name;
        float Phi, Math, Ave;
    public:
        void Get(string &name, float &phi, float &math, float &ave)
        {
            name = Name;
            phi = Phi;
            math = Math;
            Ave = ave;
        }
        void Put(string name ,float phi, float math)
        {
            Name=name;
            Phi=phi;
            Math=math;
        }
        void Display()
        {
            cout << Name << '\t' << Phi << '\t' << Math << '\t' << Ave << endl;
        }
        void Average()
        {
            Ave=(Phi+Math)/2;
        }
};

int main() {
    CStudent stud1;
    stud1.Put("Zhang San", 90, 100);
    // stud1.Phi = 90; // 错误,在此不能访问 private 数据成员
    stud1.Average();
    stud1.Display();
    return 0;
}

学生数据均为私有数据成员,外部只能通过公共成员函数对其进行访问,从而保证了数据的安全性。

1.2、基本概念

C++ 语言的早期版本被命名为 “带类的C”

(1)类

class 类名 // 类头
{ // 类体
	private:
		// 私有数据成员和成员函数;
	protected:
		// 保护数据成员和成员函数;
	public:
		// 公有数据成员和成员函数;
};

一般类名的首字母用大写字母表示

定义:

  • 类是一个用户定义的数据类型,它允许我们将数据(属性)和行为(方法)封装在一起,形成一个自定义的数据结构。
  • 类是抽象的,不占用内存空间,它定义了一组数据和操作,用于创建具体实例(instance)。

组成:

  • 数据成员(属性):类中定义的数据变量,用于描述对象的状态。它们可以是任何有效的数据类型,包括基本类型(如int、float)和自定义类型。
  • 成员函数(方法):类中定义的函数,用于定义对象的行为。它们可以访问和操作对象的数据成员,并提供对外的接口。

访问权限:
类中的成员(包括数据成员和成员函数)可以通过访问修饰符(public、private、protected)来控制其访问权限。

  • public:公有成员,可以在任何地方访问。
  • private:私有成员,只能在类内部访问(类本身的成员函数,友元函数,友元类的成员函数)。如果没有指明访问级别,那么编译系统默认为 private。
  • protected:保护成员,可以在类内部(同 private)和派生类中访问。

公有成员通常只定义函数,这些函数提供了使用这个类的外部接口

特性:

  • 封装(Encapsulation):将数据和对数据的操作封装在一起,只对外暴露必要的接口,隐藏实现细节。封装特性事实上隐蔽了程序设计的复杂性,提高了代码重用性,降低了软件开发的难度。
  • 继承(Inherit):允许一个类继承另一个类的属性和方法,实现代码复用和扩展。体现了类与类的层次关系。继承是可以传递的,体现了自然界和社会中特殊与一般的关系。
  • 多态(Polymorphism):允许不同类的对象对同一消息做出响应,通过虚函数实现。父类中定义的属性或行为,派生类继承后,可以具有不同的数据类型或表现出不同的行为特性。
  • 抽象(Abstrat):分析和提取事物与当前目标有关的本质特征,而忽略与当前目标无关的非本质特征,找出事物共性。类提供了一种抽象手段,可以隐藏内部实现细节,只显示对象的关键特征。数据抽象和行为抽象。将客观事物抽象成类及对象是比较难的过程,也是面向对象程序设计必须面对的首要问题。

(2)对象

定义:

  • 对象是类的实例,是具体的、有形的实体。通过类定义,我们可以创建多个对象,它们共享相同的行为,但可能具有不同的状态。

组成:

  • 对象由类的数据成员和成员函数组成,每个对象都有自己的状态(由数据成员表示)和行为(由成员函数表示)。

创建和使用:

  • 在C++中,可以通过直接创建、使用初始化列表或动态分配等方式来创建对象。
  • 对象之间可以通过成员函数、指针、引用等方式进行交互。

生命周期:

  • 对象的生命周期包括创建、使用和销毁三个阶段。在创建阶段,对象被分配内存并初始化;在使用阶段,对象的状态和行为可以通过其成员函数进行访问和修改;在销毁阶段,对象的内存被释放,析构函数被调用以执行必要的清理操作。

(3)类和对象的关系

类是对象的模板:类定义了一组数据和操作,用于创建具体实例;而对象是类的一个特定实例,它包含了类中定义的数据和操作。

类是抽象的,对象是具体的:类不占用内存空间,而对象占用存储空间。

一个类可以实例化多个对象:每个对象都是从一个类实例化的,并继承了该类的数据和操作。

eg 8-4 类成员的访问控制权限

#include <iostream>
using namespace std;

class Time{
    private:
        int hour;
        int minute;
        int sec;
    
    public:
        void set_time(); // 函数声明
        void show_time(); // 函数shengming

};

void Time::set_time(){  // 定义成员函数,向数据成员赋值
    cin >> hour;
    cin >> minute;
    cin >> sec;
}

void Time::show_time(){ // 定义成员函数,输出数据成员的值
    cout << hour << ":" << minute << ":" << sec << endl;
}

int main() {
    Time time;
    // time.hour=8; // error, 私有成员无法访问
    // time.minute=10; // error, 私有成员无法访问
    time.set_time();
    time.show_time();
    return 0;
}

output

12 13 14
12:13:14

1.3、成员函数的定义

在类体内定义成员函数和在类体外定义成员函数

(1)在类体内定义成员函数

一般来说,在类体内定义的成员函数规模都比较小

#include <iostream>
using namespace std;

class Time
{
    private:
        int hour;
        int minute;
        int sec;
    
    public:
        Time() // 构造函数
        {
            hour = 12;
            minute = 13;
            sec = 14;
        }
        void set_time(int h, int m, int s)
        {  // 定义成员函数,向数据成员赋值
            hour=h;
            minute=m;
            sec=s;
        }   

        void show_time()
        { // 定义成员函数,输出数据成员的值
            cout << hour << ":" << minute << ":" << sec << endl;
        }
};

int main() 
{
    Time time;
    time.show_time();
    time.set_time(13,14,15);
    time.show_time();
    return 0;
}

output

12:13:14
13:14:15

(2)在类体外定义成员函数

返回类型 类名::成员函数名(参数说明)

这个时候要用作用域运算符 :: 来指定成员函数属于哪个类

#include <iostream>
using namespace std;

class Time
{
    private:
        int hour;
        int minute;
        int sec;
    
    public:
        Time() // 构造函数
        {
            hour = 12;
            minute = 13;
            sec = 14;
        }
        void set_time(int h, int m, int s);
        void show_time();

};

void Time::set_time(int h, int m, int s)
{  // 定义成员函数,向数据成员赋值
    hour=h;
    minute=m;
    sec=s;
}   

void Time::show_time()
{ // 定义成员函数,输出数据成员的值
    cout << hour << ":" << minute << ":" << sec << endl;
}

int main() 
{
    Time time;
    time.show_time();
    time.set_time(13,14,15);
    time.show_time();
    return 0;
}

output

12:13:14
13:14:15

若在类体内没有明确指明成员的访问权限,则默认的访问权限为私有 private

关键词 private、public、protected 在类中使用先后次序无关紧要,且可使用多次。每个关键词为类成员所确定的权限从该关键词开始到下一个关键词结束。

因为类是一种数据类型,系统不会为其分配内存空间,所以在定义类中的数据成员时,不能对其进行初始化,对类中非 static 数据成员的初始化通常使用构造函数进行

1.4、内联成员函数

一、内联成员函数的概念

在 C++ 中,内联成员函数是一种特殊的成员函数。

它在编译时被展开,即将函数调用替换为函数体本身。

这意味着在程序运行时,不会发生函数调用的开销,如栈的维护、参数的传递和返回值的处理等。

内联成员函数通常用于短小且频繁调用的函数,以提高程序的执行效率。

二、内联成员函数的声明与定义

隐式内联:在C++中,当成员函数的定义直接写在类的声明中时,该函数默认是内联函数,无需显式使用 inline 关键字。

例如:

class MyClass {
public:
    int add(int a, int b) { // 隐式内联成员函数
        return a + b;
    }
};

显式内联:如果成员函数的定义在类外,可以通过在函数定义前加上 inline 关键字来显式声明为内联函数。

例如:

class MyClass {
public:
    int add(int a, int b); // 类内声明
};
 
inline int MyClass::add(int a, int b) { // 类外显式内联定义
    return a + b;
}

例如

#include <iostream>
using namespace std;

class Time
{
    private:
        int hour;
        int minute;
        int sec;
    
    public:
        Time() // 构造函数
        {
            hour = 12;
            minute = 13;
            sec = 14;
        }

        inline void set_time(int h, int m, int s)  // 不管有无 inline,都是内联函数
        {  // 定义成员函数,向数据成员赋值
            hour=h;
            minute=m;
            sec=s;
        }   

        inline void show_time();
};

inline void Time::show_time()  // 在类体外,定义内联函数需要显示声明 inline
{ // 定义成员函数,输出数据成员的值
    cout << hour << ":" << minute << ":" << sec << endl;
}

int main() 
{
    Time time;
    time.show_time();
    time.set_time(13,14,15);
    time.show_time();
    return 0;
}

output

12:13:14
13:14:15

三、内联成员函数的使用场景

短小且频繁调用的函数:对于短小且频繁调用的函数,内联化可以显著减少函数调用的开销,提高程序的执行效率。

类的访问器和修改器:类的访问器(getter)和修改器(setter)函数通常非常短小且频繁调用,适合定义为内联函数。

简单的数学运算函数:如加法、减法、乘法等简单的数学运算函数,也适合定义为内联函数。

四、内联成员函数的优缺点

优点:

  • 提高程序执行效率:通过减少函数调用的开销,内联成员函数可以提高程序的执行效率。
  • 增强代码复用性:内联成员函数允许程序员将复杂的计算或操作封装成一个类似函数的形式,使主程序逻辑更加清晰。

缺点:

  • 代码膨胀:内联化后,每个调用点都会复制一份函数体的代码,导致代码量增加,可能导致程序的内存占用增加。
  • 编译时间增加:由于内联化需要在编译时进行代码展开和优化,因此会增加编译时间。
  • 调试困难:内联化后的代码难以进行断点调试和单步执行,因为函数调用已经被替换为代码片段。

五、使用内联成员函数的注意事项

  • 函数体大小:内联函数通常适用于短小的函数。较大的函数由于代码量大,内联化后可能导致代码膨胀,因此编译器可能会拒绝内联化。
  • 函数的复杂性:复杂的函数(如包含循环、递归、复杂的条件判断等)可能难以被内联化,因为内联化后可能会引入过多的代码和复杂性。
  • 编译器的优化策略:不同的编译器可能有不同的优化策略,对内联化的处理也可能不同。因此,即使使用了 inline 关键字,也不能保证函数一定会被内联化。
  • 定义位置:内联成员函数的定义通常需要放在头文件中,这样在多个源文件使用该内联函数时,编译器才能在每个调用点都能获取到函数体进行内联展开。

C++中的内联成员函数是一种优化手段,旨在通过减少函数调用的开销来提高程序的执行效率。然而,内联成员函数并非适用于所有情况,其使用需要谨慎。在决定内联化一个成员函数之前,应综合考虑函数的复杂性、调用频率以及编译器的优化策略等因素。同时,应注意避免过度优化和代码膨胀等问题。

inline 说明对编译器来讲只是一种建议,编译器可以选择忽略这个建议。

2、对象

2.1、对象的定义

第一种方法,先定义类类型,然后再定义对象

class 类名
{
	成员表;
};
class 类名 对象名列表;

第二种方法,在定义类类型的同时定义对象

class 类名
{
	成员表;
}对象名列表;

eg

class Date
{
	private:
		...
	public:
		...
}date1, date2;

第三种方法,不出现类名,直接定义对象

class
{
	成员表;
}对象名列表;

eg

class
{
	private:
		...
	public:
		...
}date1, date2;

第三种缺乏灵活性

2.2、成员访问

一个对象的成员就是该对象的类所定义的成员,包括数据成员和成员函数。

类作用域又称类域,在类域中定义的数据成员不能使用 autoregisterextern 等修饰符,只能用 static 修饰符

对于数据成员的访问表示如下:

对象名.成员名 // 数据成员访问

对象指针名->成员名

或者

(*对象指针名).成员名

对于成员函数的访问表示如下:

对象名.成员函数名(参数表)

或者

对象指针名->成员函数名(参数表)

或者

(*对象指针名).成员函数名(参数表)

eg

#include <iostream>
using namespace std;

class Time
{
    private:
        int hour;
        int minute;
        int sec;
    
    public:
        Time() // 构造函数
        {
            hour = 12;
            minute = 13;
            sec = 14;
        }

        inline void set_time(int h, int m, int s)  // 不管有无 inline,都是内联函数
        {  // 定义成员函数,向数据成员赋值
            hour=h;
            minute=m;
            sec=s;
        }   

        inline void show_time();
};

inline void Time::show_time()  // 在类体外,定义内联函数需要显示声明 inline
{ // 定义成员函数,输出数据成员的值
    cout << hour << ":" << minute << ":" << sec << endl;
}

int main() 
{
    Time time, *t;
    t = &time;
    t->show_time();
    (*t).set_time(13,14,15);
    time.show_time();
    return 0;
}

output

12:13:14
13:14:15

3、构造函数

3.1、构造函数的定义

构造(Constructor)函数在对象被创建时由编译系统自动调用,用于完成成员的初始化工作。

构造函数是一种特殊的成员函数,它有如下特性

  • 名称与类名相同:构造函数的名称必须与类名完全一致,以便编译器能够识别。
  • 无返回类型:构造函数没有返回类型,包括 void 类型。
  • 自动调用:在创建对象时,构造函数会自动被调用,且只调用一次。
  • 支持重载:一个类可以有多个构造函数,通过参数个数、类型或顺序等进行重载,以满足不同的初始化需求。

构造函数最好是 public 的,private 构造函数不能直接用来初始化对象。


构造函数的种类

(1)默认构造函数

定义:无参数的构造函数。

特点:如果类中没有显式定义构造函数,编译器会自动生成一个默认的无参构造函数,该构造函数不做任何工作。如果定义了其他构造函数,编译器则不再自动生成默认构造函数。

使用场景:适用于不需要初始化参数的情况,或者为成员变量提供默认值。

C++规定,每个类必须至少有一个构造函数,没有显示定义,系统会提供一个无参数的构造函数(缺省的构造函数)

(2)带参数的构造函数

定义:接受一个或多个参数的构造函数。

特点:在创建对象时,可以传入参数来初始化成员变量。

使用场景:适用于需要在创建对象时初始化成员变量的情况。

(3)拷贝构造函数

定义:用于复制一个已有对象来初始化新对象的构造函数。

特点:参数为同类对象的引用。如果类中没有显式定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数,执行浅拷贝。

使用场景:当需要用一个对象来初始化另一个同类对象时,如对象赋值、函数参数传递、函数返回值等。

(4)移动构造函数(C++11及以后)

定义:允许资源的所有权从一个对象转移到另一个对象的构造函数。

特点:通过右值引用实现,主要用于优化临时对象的资源管理。

使用场景:适用于处理临时对象,提高资源管理的效率。

(5)委托构造函数(C++11及以后)

定义:允许一个构造函数调用同一个类的另一个构造函数的构造函数。

特点:可以减少代码重复,提高代码的可读性和可维护性。

使用场景:当类有多个构造函数且它们之间有共同的初始化代码时,可以使用委托构造函数来避免代码重复。


eg:8-9 无参构造函数

#include <iostream>
using namespace std;

class Time
{
    private:
        int hour;
        int minute;
        int sec;
    
    public:
        Time() // 无参构造函数
        {
            hour = 12;
            minute = 13;
            sec = 14;
        }

        inline void set_time(int h, int m, int s)  // 不管有无 inline,都是内联函数
        {  // 定义成员函数,向数据成员赋值
            hour=h;
            minute=m;
            sec=s;
        }   

        inline void show_time();
};

inline void Time::show_time()  // 在类体外,定义内联函数需要显示声明 inline
{ // 定义成员函数,输出数据成员的值
    cout << hour << ":" << minute << ":" << sec << endl;
}

int main() 
{
    Time time, *t;
    t = &time;
    t->show_time();
    (*t).set_time(13,14,15);
    time.show_time();
    return 0;
}

output

12:13:14
13:14:15

eg 8-10 带有参数的构造函数

#include <iostream>
using namespace std;

class Cuboid
{
    private:
        int height;
        int width;
        int length;
    
    public:
        Cuboid(int, int, int); // 带有三个参数的构造函数
        int volume();
};

Cuboid::Cuboid(int h, int w, int l)
{
    height = h;
    width = w;
    length = l;
}

int Cuboid::volume()
{
    return (height * width * length);
}

int main() 
{
    Cuboid c1(15, 45, 30);
    cout << "Cuboid c1 的体积为:" << c1.volume() << endl;

    Cuboid c2(10, 30, 22);
    cout << "Cuboid c2 的体积为:" << c2.volume() << endl;

    return 0;
}

output

Cuboid c1 的体积为:20250
Cuboid c2 的体积为:6600

构造函数自动调用


eg 8-11 构造函数重载

#include <iostream>
using namespace std;

class Cuboid
{
    private:
        int height;
        int width;
        int length;
    
    public:
        Cuboid(); // 无参构造函数
        Cuboid(int, int, int); // 带有三个参数的构造函数
        int volume();
};


Cuboid::Cuboid()
{
    height = 15;
    width = 15;
    length = 15;
}

Cuboid::Cuboid(int h, int w, int l)
{
    height = h;
    width = w;
    length = l;
}

int Cuboid::volume()
{
    return (height * width * length);
}

int main() 
{
    Cuboid c1(15, 45, 30);
    cout << "Cuboid c1 的体积为:" << c1.volume() << endl;

    Cuboid c2(10, 30, 22);
    cout << "Cuboid c2 的体积为:" << c2.volume() << endl;

    Cuboid c3;
    cout << "Cuboid c3 的体积为:" << c3.volume() << endl;

    return 0;
}

output

Cuboid c1 的体积为:20250
Cuboid c2 的体积为:6600
Cuboid c3 的体积为:3375

注意:尽管有多个构造函数,但是对于任一个对象来说,建立对象时只执行其中的一个构造函数,并非每个构造函数都被执行。


eg 8-12 使用缺省值的构造函数

#include <iostream>
using namespace std;

class Cuboid
{
    private:
        int height;
        int width;
        int length;
    
    public:
        Cuboid(int, int, int); // 带有三个参数的构造函数
        int volume();
};

Cuboid::Cuboid(int h=15, int w=15, int l=15)
{
    height = h;
    width = w;
    length = l;
}

int Cuboid::volume()
{
    return (height * width * length);
}

int main() 
{
    Cuboid c1(25);
    cout << "Cuboid c1 的体积为:" << c1.volume() << endl;

    Cuboid c2(25, 40);
    cout << "Cuboid c2 的体积为:" << c2.volume() << endl;

    Cuboid c3(25, 30, 40);
    cout << "Cuboid c3 的体积为:" << c3.volume() << endl;

    Cuboid c4;
    cout << "Cuboid c4 的体积为:" << c4.volume() << endl;

    return 0;
}

output

Cuboid c1 的体积为:5625
Cuboid c2 的体积为:15000
Cuboid c3 的体积为:30000
Cuboid c4 的体积为:3375

构造函数的使用注意事项

  • 避免复杂的初始化逻辑:构造函数中应尽量避免复杂的逻辑或可能会抛出异常的操作,以免导致对象处于不确定状态。
  • 管理动态资源:如果构造函数中分配了动态资源(如内存或文件句柄),必须在析构函数中释放这些资源,以避免资源泄漏。
  • 合理使用默认构造函数:如果不需要自定义初始化逻辑,可以接受编译器提供的默认构造函数。如果需要自定义初始化逻辑,应显式定义默认构造函数。

3.2、子对象与构造函数

在定义一个新类时,可以把一个已定义类的对象作为该类的数据成员,这个类对象被称为子对象。

完成子对象成员的初始化,必须通过调用子对象成员的构造函数来实现。

#include <iostream>
using namespace std;

class Rectangle // 定义矩形类
{
    private:
        int Width;
        int Length;
    public:
        Rectangle(int w, int l); // 定义带参数的构造函数
        int Area();
};

Rectangle::Rectangle(int w, int l)
{
    Width = w;
    Length = l;
}

int Rectangle::Area()
{
    return (Width * Length);
}

class Cuboid // 定义长方体类
{
    private:
        int Height;
        Rectangle r;
    
    public:
        Cuboid(int w, int l, int h):r(w, l)
        {
            Height = h;
        }
        int Volume();

};

int Cuboid::Volume()
{
    return (r.Area() * Height);
}


int main() 
{
    Cuboid c1(10, 20, 100);
    cout << "Cuboid c1 的体积为:" << c1.Volume() << endl;
    return 0;
}

output

Cuboid c1 的体积为:20000

子对象可以有多个,它们构造函数的调用顺序取决于这些子对象成员在类中的说明顺序,而与它们的成员初始化表的位置无关

定义类的对象时,先调用各个子对象成员的构造函数,初始化相应的子对象成员,然后再执行类的构造函数,初始化类中其他成员。析构函数的调用顺序与构造函数正好相反。

3.3、拷贝构造函数

定义:拷贝构造函数是一种特殊的构造函数,用于创建一个新对象,该对象是另一个已存在对象的副本。

参数:拷贝构造函数的参数为本类对象的引用,通常是 const 引用,以防止在拷贝过程中修改原对象。

形式:拷贝构造函数的一般形式为 class_name(const class_name& old_obj);,其中 class_name 是类的名称,old_obj 是已存在对象的引用。

eg 8-14 同类对象拷贝

一般形式为 对象名 1 = 对象名 2;,对象名 1 和对象名 2 必须属于同一个类

#include <iostream>
using namespace std;

class Sample
{
    private:
        int nTest;
    public:
        Sample(int n)
        {
            nTest=n;
        }

        int readtest()
        {
            return nTest;
        }
};

int main() 
{
    Sample S1(100);
    Sample S2 = S1;
    cout << S2.readtest() << endl;

    return 0;
}

output

100

虽然没有定义拷贝函数,但是系统会自动提供一个默认的拷贝构造函数来完成拷贝工作。


eg 8-15 新定义的拷贝构造函数

#include <iostream>
using namespace std;

class Sample
{
    private:
        int nTest;
    public:
        Sample(int n) // 构造函数
        {
            nTest=n;
        }

        Sample(Sample &S) // 自定义拷贝构造函数
        {
            cout << "copy constructor" << endl;
            nTest = S.nTest + 8;
        }

        int readtest()
        {
            return nTest;
        }
};

int main() 
{
    Sample S1(100);
    Sample S2 = S1;
    cout << S2.readtest() << endl;

    return 0;
}

output

copy constructor
108

上述代码中,Sample(Sample &S) 就是自定义的拷贝构造函数

C++ 规定,拷贝构造函数的名称必须与类名称一致,函数的形式参数是本类型的一个引用变量,且必须是引用

系统既然可以帮我们写拷贝构造函数,那我们自定义拷贝构造函数的意义是什么呢?

有时候不一定都是将原对象的数据成员全部的、原封不动的赋给新对象,可以有选择、有变化的拷贝,这个时候就需要自定义构造函数了。


拷贝构造函数在以下三种情况下会被调用:

  • 对象作为函数参数传递时:当函数参数是通过值传递而不是引用传递时,参数的对象将会被拷贝。此时,拷贝构造函数用于创建函数内部使用的对象副本。
  • 函数返回对象时:当函数的返回类型是类类型,且返回的是一个对象时,这个对象将会被拷贝到调用函数时的上下文中。拷贝构造函数用于创建这个返回对象的副本。
  • 对象通过初始化创建时:当一个对象使用另一个同类型对象进行初始化时,拷贝构造函数将被调用。例如,MyClass obj2 = obj1;,这里 obj2 是通过调用拷贝构造函数用 obj1 初始化的。

实现方式

  • 浅拷贝:默认的拷贝构造函数执行的是浅拷贝,即按位拷贝对象的每个成员变量。对于基本数据类型,浅拷贝是足够的;但对于包含指针或动态分配内存的类,浅拷贝可能会导致多个对象共享同一资源,从而引发问题。

  • 深拷贝:为了避免浅拷贝带来的问题,程序员可以显式定义拷贝构造函数,实现深拷贝。深拷贝会为对象所拥有的资源创建新的副本,确保新对象和原对象是完全独立的。

注意事项

  • 避免无限递归:拷贝构造函数的参数必须是引用,不能是值传递。 如果参数是值传递,那么在调用拷贝构造函数时又需要拷贝参数对象,这将导致无限递归调用,最终引发栈溢出。

  • 自定义拷贝构造函数:如果类中包含指针或动态分配的内存,通常需要自定义拷贝构造函数,实现深拷贝,以避免资源冲突和内存泄漏问题。

  • 拷贝省略:在某些情况下,编译器可能会通过“拷贝省略”(copy elision)优化来省略拷贝构造函数的调用。例如,当编译器能够确定两个对象指向同一块内存时,可以直接让新对象与原对象共享内存,而无需调用拷贝构造函数。

4、析构函数

4.1、析构函数的定义

析构(Destructor)函数是当对象脱离其作用域(例如对象所在的函数已调用完毕),系统自动执行的。

析构函数的主要作用是清理对象占用的资源和进行必要的善后工作,比如释放动态分配的内存、关闭文件等。

格式

class 类名
{
	public:
	~类名() // 析构函数
	{
		// 函数体
	}
};

特性

  • 名称:析构函数在函数名前面加一个 ~ 位取反符

  • 没有参数:析构函数不接受任何参数,也没有 void 返回值。

  • 自动调用:当对象离开其作用域或者被显式销毁时(如通过 delete 操作符),析构函数会被自动调用。

  • 每个类只能有一个析构函数,不能重载。

  • 如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数,它也不进行任何操作

  • 在析构函数内要终止程序执行,不能使用 exit() 函数,只能使用 abort() 函数,因为 exit 终止前会调用析构函数,形成无休止的递归

eg 8-16 析构函数

#include <iostream>
#include <string.h>
using namespace std;

class CStudent
{
    private:
        int Num;
        string Name;
    
    public:
        CStudent(int num, string name)
        {
            Num = num;
            Name = name;
            cout << "Constructor called." << endl;
        }
        ~CStudent()
        {
            cout << Name << " Destructor called." << endl;
        }
        void display()
        {
            cout << "学号:" << Num << endl;        
            cout << "姓名:" << Name << endl;        
        }
};


int main() 
{
    CStudent s1(10100, "Zhang San");
    s1.display();

    CStudent s2(10101, "Li Si");
    s2.display();
    return 0;
}

output

Constructor called.
学号:10100
姓名:Zhang San
Constructor called.
学号:10101
姓名:Li Si
Li Si Destructor called.
Zhang San Destructor called.

对象数组声明期结束时,对象数组的每个元素的析构函数都会被调用

析构函数在对象作为函数返回值后被调用。

函数调用过程中,在临时对象生成的时候会有构造函数被调用,临时对象消亡导致析构函数调用

eg 8-17 对象数组析构函数

#include <iostream>
#include <string.h>
using namespace std;

class CStudent
{
    private:
        int Num;
        string Name;
    
    public:
        CStudent(int num, string name)
        {
            Num = num;
            Name = name;
            cout << Name << " Constructor called." << endl;
        }
        ~CStudent()
        {
            cout << Name << " Destructor called." << endl;
        }
        void display()
        {
            cout << "学号:" << Num << endl;        
            cout << "姓名: " << Name << endl;        
        }
};


int main() 
{
    CStudent s[2] = {{10100, "Zhang San"}, {10101, "Li Si"}};
    s[0].display();
    s[1].display();
    return 0;
}

output

Zhang San Constructor called.
Li Si Constructor called.
学号:10100
姓名: Zhang San
学号:10101
姓名: Li Si
Li Si Destructor called.
Zhang San Destructor called.

eg 8-18 临时对象

#include <iostream>
using namespace std;

class CMyclass
{
    public:
        ~CMyclass()
        {
            cout << "Destructor Called." << endl;
        }
};

CMyclass func(CMyclass obj)
{
    return obj;
}


int main() 
{

    CMyclass cls;
    cls = func(cls);
    return 0;
}

output

Destructor Called.
Destructor Called.
Destructor Called.

析构函数被调用了三次

实例化对象一次,形参传递一次,函数返回一次

4.2、构造函数和析构函数的调用顺序

在一般情况下,调用析构函数的次序正好与调用构造函数的次序相反

但是,并不是在任何情况下都是按这一原则处理的。

(1)对于全局定义的对象(函数体外定义的对象),在程序开始执行时(包括 main()在内的所有函数执行之前)调用构造函数,到程序结束或者调用 exit() 函数终止程序时才调用析构函数

(2)对于局部定义的对象(函数体内定义的对象),在程序执行到定义对象的地方时调用构造函数,到函数结束时才调用析构函数。

(3)用 static 定义的局部对象,在首次到达对象定义位置时调用构造函数,在程序结束时调用析构函数

(4)对于用 new 运算符动态生成的对象,在产生对象时调用构造函数,只有用 delete 释放对象时,才调用析构函数。若不使用 delete 运算符来撤销动态生成的对象,则析构函数不会被调用

eg 8-19 构造函数和析构函数的调用

#include <iostream>
#include <string.h>
using namespace std;

class CStudent
{
    private:
        string Name;
        float Score;

    public:
        CStudent(string name, float score);
        ~CStudent();
};

CStudent::CStudent(string name, float score)
{
    Name = name;
    Score = score;
    cout << Name << " Constructor called." << endl;
}

CStudent::~CStudent()
{
    cout <<  Name << " Destructor Called." << endl;
}

int main() 
{
    CStudent st1("Zhang San", 99);
    CStudent st2("Li Si", 88);
    CStudent st[2] = {st1, st2};
    return 0;
}

output

Zhang San Constructor called.
Li Si Constructor called.
Li Si Destructor Called.
Zhang San Destructor Called.
Li Si Destructor Called.
Zhang San Destructor Called.

赋值 st[0] 和 st[1] 则是调用了默认的拷贝构造函数,没有打印,调用析构函数的时候出现打印

4.3、对象的动态建立与释放

当对象是通过 new 操作符动态分配时,需要显式使用 delete 操作符来销毁对象,从而调用析构函数。

如果不使用 delete 运算符来撤销动态生成的对象,程序结束时将不会调用析构函数。

eg 8-20 new 和 delete

#include <iostream>
using namespace std;

class Complex
{
    private:
        double real;
        double imag;
    
    public:
        Complex(double r, double i)
        {
            real = r;
            imag = i;
            cout << "Constructor called." << endl;
        }

        ~Complex()
        {
            cout << "Destructor called." << endl;
        }

        void display()
        {
            cout << "(" << real << "," << imag << "i)" << endl;
        }
};


int main() 
{
    Complex *p = new Complex(3,4);
    p->display();
    delete p;
    return 0;
}

output

Constructor called.
(3,4i)
Destructor called.

如果不显示的 delete,则不会调用析构函数

int main() 
{
    Complex *p = new Complex(3,4);
    p->display();
    //delete p;
    return 0;
}

output

Constructor called.
(3,4i)

如果分配的是数组,则使用 delete[] 来释放。

注意事项

(1)内存管理

使用 new 分配的内存必须使用 delete 释放,否则会导致内存泄漏。

确保每个 new 对应一个 delete,每个 new[] 对应一个 delete[]

(2)指针安全

在释放内存后,最好将指针设置为 nullptr,以避免悬空指针问题。

例如:delete student1; student1 = nullptr;

(3)异常处理

在动态内存分配时,考虑使用异常处理机制来捕获可能的内存分配失败(尽管在现代操作系统中,内存分配失败非常罕见)。

(4)拷贝构造和赋值

如果类中有动态内存分配,建议显式定义拷贝构造函数和赋值操作符,以避免浅拷贝带来的问题。

(5)智能指针

在 C++11 及之后的标准中,引入了智能指针(如 std::unique_ptrstd::shared_ptr),用于自动管理动态内存,减少手动管理内存的复杂性和错误。

5、静态成员

声明为 static 的类成员称为静态成员,可以被类的所有对象共享。

静态数据成员、静态成员函数

5.1、静态数据成员

静态数据成员是属于类本身而不是某个特定对象的成员。这意味着所有该类的对象共享同一个静态数据成员。静态数据成员在类的所有实例之间共享,并且它们的生命周期贯穿程序的整个运行周期。

静态数据成员的存储空间不会随着对象的产生而分配,也不会随着对象的消失而释放。因此,静态数据成员不能在类体内进行初始化,而只能在类体内进行声明,在类体外进行初始化。

声明和定义:

  • 静态数据成员在类内声明,并在类外定义。
  • 在类内声明时,使用关键字 static。
  • 在类外定义时,不需要使用 static 关键字,但需要使用类名进行限定。

初始化:

  • 静态数据成员通常在类外进行初始化。
  • 它们可以用常量表达式进行初始化,也可以在类外的定义中初始化。
  • 初始化的基本格式为:数据类型名 类名::静态数据成员名=初值;

访问

  • 静态数据成员可以通过类名直接访问,也可以通过对象访问。
  • 语法为 ClassName::memberName 或者 对象名.memberName

共享性

  • 因为静态数据成员在类的所有对象中共享,所以对一个对象中静态成员的修改会影响到该类的所有对象。

存储类

  • 静态数据成员具有静态存储类,这意味着它们在程序开始时被加载,并在程序结束时被卸载。
#include <iostream>
using namespace std;

class CStudent
{
    private:
        string SName;
        float Score;
        static int studentTotal; // 静态数据成员声明,保存学生的总人数
        static float sumScore; // 静态数据成语声明,保存所有学生的成绩和    
    public:
        CStudent(string name, float score);
        static float average(); // 计算学生的平均分
        void Print(); // 打印学生的姓名和成绩
        ~CStudent();  //析构函数,当减少一个对象的时候,studentTotal 减 1
};

CStudent::CStudent(string name, float score)
{
    SName = name;
    Score = score;
    studentTotal++;  // 学生人数+1
    sumScore+=Score; // 总分数增加
    cout << SName << " Constructor called." << endl;
}

CStudent::~CStudent()
{
    studentTotal--; // 学生人数-1
    sumScore-=Score; // 总分数减少
    cout << SName << " Destructor called." << endl;
}
 
void CStudent::Print()
{
    cout << SName << ":" << Score << endl;
}

float CStudent::average()
{
    return (sumScore / studentTotal);

}

int CStudent::studentTotal = 0;  // 静态数据成员的初始化必须在类外进行
float CStudent::sumScore = 0;  //注意,不要加 static 关键字

int main() 
{
    CStudent st1("Zhang San", 98);
    CStudent st2("Li Si", 85);
    st1.Print();
    st2.Print();
    cout << "平均分为:" << CStudent::average() << endl; // 调用静态成员函数

    return 0;
}

output

Zhang San Constructor called.
Li Si Constructor called.
Zhang San:98
Li Si:85
平均分为:91.5
Li Si Destructor called.
Zhang San Destructor called.

上面的例子静态数据成员定义成了 private 形式,对象不能直接访问,定义成 public 就可以

eg 8-21

#include <iostream>
using namespace std;

class CStudent
{
    private:
        string SName;
        float Score;

    public:
        static int studentTotal; // 静态数据成员声明,保存学生的总人数
        static float sumScore; // 静态数据成语声明,保存所有学生的成绩和    
        CStudent(string name, float score); // 构造函数
        static float average(); // 计算学生的平均分
        void Print(); // 打印学生的姓名和成绩
        ~CStudent();  //析构函数,当减少一个对象的时候,studentTotal 减 1
};

CStudent::CStudent(string name, float score)
{
    SName = name;
    Score = score;
    studentTotal++;  // 学生人数+1
    sumScore+=Score; // 总分数增加
    cout << SName << " Constructor called." << endl;
}

CStudent::~CStudent()
{
    studentTotal--; // 学生人数-1
    sumScore-=Score; // 总分数减少
    cout << SName << " Destructor called." << endl;
}
 
void CStudent::Print()
{
    cout << SName << ":" << Score << endl;
}

float CStudent::average()
{
    return (sumScore / studentTotal);

}

int CStudent::studentTotal = 0;  // 静态数据成员初始化必须在类外进行
float CStudent::sumScore = 0;  //注意,不要加 static 关键字

int main() 
{
    CStudent st1("Zhang San", 98);
    cout << st1.sumScore << " " << CStudent::sumScore << endl;
    CStudent st2("Li Si", 85);
    cout << st2.sumScore << " " << CStudent::sumScore << endl;  // 所有对象数据共享,对一个对象中静态成员的修改会影响到该类的所有对象
    st1.Print();
    st2.Print();
    cout << "平均分为:" << CStudent::average() << " " << st2.average() << endl; // 调用静态成员函数
    
    return 0;
}

output

Zhang San Constructor called.
98 98
Li Si Constructor called.
183 183
Zhang San:98
Li Si:85
平均分为:91.5 91.5
Li Si Destructor called.
Zhang San Destructor called.

说明

(1)静态数据成员存储空间的分配是在一个程序一开始运行时就被分配的,并不是在程序运行过程中在某一个函数内分配空间和初始化

(2)静态数据成员初始化语句应当写在程序的全局区域中,并且必须指明其数据类型与所属类名。(写在主函数中会编译不通过)

(3)如果未对静态数据成员赋初值,则编译系统会自动赋予初值 0;

eg

#include <iostream>

class MyClass {
public:
    static int staticVar;  // 静态数据成员声明

    static void printStaticVar() {
        std::cout << "Static Variable: " << staticVar << std::endl;
    }
};

// 静态数据成员定义和初始化
int MyClass::staticVar = 0;

int main() {
    // 通过类名访问静态数据成员
    MyClass::staticVar = 10;
    MyClass::printStaticVar();

    // 通过对象访问静态数据成员
    MyClass obj1, obj2;
    obj1.staticVar = 20;
    obj2.printStaticVar();  // 输出将是 20,因为是共享的

    return 0;
}

output

Static Variable: 10
Static Variable: 20

5.2、静态成员函数

静态成员函数是属于类本身而不是某个特定对象的成员函数。与静态数据成员类似,静态成员函数可以在没有对象的情况下调用。它们主要用于处理与类相关的操作,而不是与特定对象相关的操作。

静态成员函数没有 this 指针,通常它只访问属于全体对象的成员——静态成员,也可以访问全局变量

一般情况下,静态成员函数不访问类的非静态成员,因为这些非静态成员是属于特定对象的。

eg 8-21 中,类 CStudent 的静态成员函数 static float average() 定义如下

float CStudent::average()
{
    return (sumScore / studentTotal);

}

使用了静态数据成成员 sumScorestudentTotal

如果将 void Print(); 成员函数声明为静态成员函数 static void Print();,编译会出现错误,因为在函数体中的语句 cout << SName << ":" << Score << endl; 中使用了非静态数据成员 SNameScore

特性

属于类本身

  • 静态成员函数属于类,而不是类的对象。
  • 它们可以在没有创建对象的情况下通过类名直接调用。

不能访问非静态成员

  • 静态成员函数不能访问类的非静态成员(包括非静态数据成员和非静态成员函数),因为它们没有 this 指针。
  • 但是,静态成员函数可以访问静态数据成员和其他静态成员函数。
  • 注意,非静态成员函数可以任意地访问静态成员函数和静态数据成员。

共享性

  • 静态成员函数在所有对象中共享,但它们不依赖于任何对象。

语法

  • 在类内声明静态成员函数时,使用关键字 static
  • 调用静态成员函数时,可以使用类名或对象来调用,但通常使用类名。

静态成员函数通常用于以下场景

工厂方法:

  • 用于创建对象实例,而不需要依赖对象本身。

实用函数:

  • 提供与类相关的实用功能,例如处理所有对象的统计信息。

访问静态成员:

  • 用于访问和修改静态数据成员。

5.3、静态成员的访问

类的公有静态数据成员既可以用类的对象访问,也可以直接用作用域运算符 :: 通过类名来访问

静态数据成员

类名::静态数据成员名 // 建议使用此形式

对象名.静态数据成员名 // 不建议,使人误以为静态数据成员是属于某个对象的

类的公有静态成员函数访问和公有静态数据成员一样,有两种形式

静态成员函数

类名::静态成员函数名 // 建议使用此形式

对象名.静态成员函数名 // 不建议使用

例子参考上述的 eg 8-21

eg

#include <iostream>

class MyClass {
private:
    static int staticVar;  // 静态数据成员

public:
    // 静态成员函数声明
    static void printStaticVar();
    static void setStaticVar(int value);
};

// 静态数据成员定义和初始化
int MyClass::staticVar = 0;

// 静态成员函数定义
void MyClass::printStaticVar() {
    std::cout << "Static Variable: " << staticVar << std::endl;
}

void MyClass::setStaticVar(int value) {
    staticVar = value;
}

int main() {
    // 通过类名调用静态成员函数
    MyClass::setStaticVar(10);
    MyClass::printStaticVar();

    // 通过对象调用静态成员函数(不推荐,但可行)
    MyClass obj;
    obj.setStaticVar(20);
    obj.printStaticVar();

    return 0;
}

output

Static Variable: 10
Static Variable: 20

6、应用实例

eg 8-22 定义描述矩形的类,用构造函数完成矩阵对象的初始化,编写计算矩形面积的函数,并且输出矩形的面积。

#include <iostream>
using namespace std;

class Rectangle
{
    private:
        int X0, Y0;  // 左上角坐标
        int Width, Length;  // 宽度、长度
    
    public:
        Rectangle(int x, int y, int width, int len)  // 带参构造函数
        {
            X0 = x;
            Y0 = y;
            Width = width;
            Length = len;
        }

        Rectangle()  // 无参构造函数
        {
            X0 = 1;
            Y0 = 2;
            Width = 10;
            Length = 20;
        }

        Rectangle(Rectangle &r)  // 拷贝构造函数
        {
            // X0 = r.X0;
            // Y0 = r.Y0;
            Width = r.Width;
            Length = r.Length;
        }

        void setXY(int x, int y)  // 设置左上角坐标
        {
            X0 = x;
            Y0 = y;
        }

        void setWH(int w, int len)  // 设置宽度和长度
        {
            Width = w;
            Length = len;
        }

        int area() // 计算面积
        {
            return (Width * Length);
        }
};

int main() 
{
    Rectangle r1(10, 20, 100, 50);
    cout << "矩形 r1 的面积是:" << r1.area() << endl;

    Rectangle r2;
    cout << "矩形 r2 的面积是:" << r2.area() << endl;

    Rectangle r3 = r2;
    cout << "矩形 r3 的面积是:" << r3.area() << endl;

    Rectangle r4(r1);
    cout << "矩形 r4 的面积是:" << r4.area() << endl;
    r4.setWH(50, 20);
    cout << "矩形 r4 set 的面积是:" << r4.area() << endl;

    return 0;
}

output

矩形 r1 的面积是:5000
矩形 r2 的面积是:200
矩形 r3 的面积是:200
矩形 r4 的面积是:5000
矩形 r4 set 的面积是:1000

拷贝构造函数自己定义的好处在于,可以自行决定要拷贝的数据成员

构造函数可以有多个

注意拷贝的形式,直接 A=B 或者 A(B)


eg 8-23 设计一个复数类,进行复数的运算。并能实现运算结果的输出。

在这里插入图片描述

#include <iostream>
using namespace std;

class Complex
{
    private:
        double Real;
        double Image;
    
    public:
        Complex(double r=0, double i=0)  // 带有默认值的构造函数
        {
            Real = r;
            Image = i;
        }

        void show();  // 显示函数
        Complex Add(Complex &);  //复数加法
        Complex Sub(Complex &);  //复数减法
        Complex Multi(Complex &);  // 复数乘法
};

void Complex::show()
{
    cout << "(" << Real << "," << Image << "i)" << endl;
}

Complex Complex::Add(Complex &c2)  //复数加法
{
    Complex c;
    c.Real = Real + c2.Real;
    c.Image = Image + c2.Image;
    return c;
}

Complex Complex::Sub(Complex &c2)  //复数减法
{
    Complex c;
    c.Real = Real - c2.Real;
    c.Image = Image - c2.Image;
    return c;
}

Complex Complex::Multi(Complex &c2)  // 复数乘法
{    
    Complex c;
    c.Real = Real * c2.Real - Image * c2.Image;
    c.Image = Real * c2.Image + Image * c2.Real;
    return c;
}

int main() 
{
    Complex c1(1, 1);
    Complex c2(1, -1);
    Complex c;
    c = c1.Add(c2);
    cout << "c1 + c2 = ";
    c.show();

    c = c1.Sub(c2);
    cout << "c1 - c2 = ";
    c.show();

    c=c1.Multi(c2);
    cout << "c1 * c2 = ";
    c.show();

    return 0;
}

output

c1 + c2 = (2,0i)
c1 - c2 = (0,2i)
c1 * c2 = (2,0i)

eg 8-24 编写一个简单的卖玩具程序。类内必须具有玩具单价、售出数量以及每种玩具售出的总金额等数据,并为该类建立一些必要的元素,并在主程序中使用对象数组建立若干个带有单价和售出数量的对象,显示每种玩具售出的总金额,并统计卖出玩具的总数量。

分析:卖出玩具的总数量是被所有对象共享的数值,应该设计成静态数据成员。

#include <iostream>
using namespace std;

class Toy
{
    private:
        int Price;  // 单价
        int Num;  // 数量
        long Total;  // 总价格
        static int NumTotal;  // 所有玩具的总数量
    
    public:
        void Input(int p, int n); // 输入单价和数量
        void Compute();  // 计算总价格
        void Print();  // 打印单价、数量、总价格
        static int getNumTotal();  // 统计所有玩具的总数量
};

void Toy::Input(int p, int n)
{
    Price = p;
    Num = n;
    NumTotal += Num;
}

void Toy::Compute()
{
    Total = (long)Price * Num;
}

void Toy::Print()
{
    cout << "价格=" << Price << ", 数量=" << Num << ", 总价=" << Total << endl;
}

int Toy::getNumTotal()
{
    return NumTotal;
}

int Toy::NumTotal = 0; // 一定别忘了定义和初始化静态数据成员

int main() 
{
    Toy * toy;
    toy = new Toy[4];
    toy[0].Input(15, 150);
    toy[1].Input(30, 55);
    toy[2].Input(10, 30);
    toy[3].Input(25, 120);

    for(int i=0; i<4; i++)
        toy[i].Compute();

    for(int i=0; i<4; i++)
        toy[i].Print();

    delete[] toy; // 注意这里释放掉申请的内存资源

    cout << endl << "卖出玩具的总数量为:"<< Toy::getNumTotal() << endl;

    return 0;
}

output

价格=15, 数量=150, 总价=2250
价格=30, 数量=55, 总价=1650
价格=10, 数量=30, 总价=300
价格=25, 数量=120, 总价=3000

卖出玩具的总数量为:355

附录——堆和栈

在C++中,堆(Heap)和栈(Stack)是两种主要的内存分配区域,它们在内存管理、性能和使用方式上有显著的区别。

一、栈(Stack)

定义与特点:

  • 栈是由操作系统自动管理的内存区域,用于存储局部变量、函数参数、返回地址等数据。
  • 栈内存的分配和回收是由编译器在程序运行时自动完成的,程序的生命周期和栈的生命周期一致。
  • 栈是一种后进先出(LIFO)的数据结构,内存分配和释放速度非常快。

内存管理:

  • 栈内存由编译器自动分配和回收,无需程序员干预。
  • 当一个函数被调用时,栈会分配空间来存储局部变量,函数返回时,这些局部变量会被自动销毁,栈内存会被释放。

优缺点:

  • 优点:自动内存管理,分配和回收内存的速度非常快,不容易发生内存泄漏问题。
  • 缺点:内存大小有限,受操作系统限制,容易发生栈溢出;只能用于存储局部变量,无法动态调整大小

使用场景:

  • 适用于存储生命周期短、大小固定的数据,如局部变量、函数参数、返回地址等。
  • 当程序需要频繁调用函数时,栈非常高效。

二、堆(Heap)

定义与特点:

  • 堆是由程序员手动管理的内存区域,用于动态分配内存(例如通过 new 或 malloc 等)。
  • 堆内存的分配和回收由程序员控制,程序员需要显式地调用 delete 或 free 来释放内存。
  • 堆是一个大的内存区域,用于存储需要在运行时动态创建的数据结构,如对象、数组等。

内存管理:

  • 堆内存的分配和回收需要程序员显式管理。
  • 使用 new 或 malloc 分配内存,使用 delete 或 free 回收内存。
  • 如果不释放堆内存,可能导致内存泄漏。

优缺点:

  • 优点:灵活,能够动态分配内存;可以分配大量内存,适用于需要动态管理资源的情况。
  • 缺点:需要手动管理内存,容易出现内存泄漏、悬挂指针等问题——(Dangling Pointer)是C++编程中常见的内存管理问题,它指的是一个指针仍然保留着之前指向的内存地址,但是这片内存区域可能已经被释放或者不再有效。分配和回收内存的速度较慢。

使用场景:

  • 适用于存储生命周期长、大小动态变化的数据,如对象、数组等。
  • 当程序需要动态创建数据结构,并且数据在不同函数之间共享时,堆是更好的选择。

三、堆与栈的区别

在这里插入图片描述

四、注意事项

  • 栈溢出:由于栈的大小有限,当递归调用过深或局部变量过大时,可能导致栈溢出。

  • 内存泄漏:堆内存需要程序员手动释放,如果忘记释放或释放不当,可能导致内存泄漏。

  • 内存碎片:频繁的堆内存分配和释放可能导致内存碎片,降低程序效率。

五、总结

在C++编程中,了解堆和栈的区别以及它们的使用场景对于编写高效、安全的代码至关重要。栈适用于存储小型、生命周期短的数据,而堆则适用于存储大型、生命周期长的数据。在实际编程中,应根据具体需求选择合适的内存分配方式,并注意避免常见的内存管理错误。

相关文章:

  • 虚拟机 | Ubuntu图形化系统: open-vm-tools安装失败以及实现文件拖放
  • 数据可视化大屏产品设计方案(附Axure源文件预览)
  • 【JavaSE-7】方法的使用
  • 【C语言】函数篇
  • 安装remixd,在VScode创建hardhat
  • 软考架构师笔记-数据库系统
  • 确认机制的分类及其区别与联系探讨
  • 在springboot项目中引入log4j 2.x
  • mysql进阶(三)
  • 【CSS 】Class Variance Authority CSS 类名管理工具库
  • JVM与性能调优详解
  • 香港电讯CE2.0网络全面升级,100G服务支援企业关键应用
  • Unity InputField + ScrollRect实现微信聊天输入框功能
  • unity学习64,第3个小游戏:一个2D跑酷游戏
  • 如何用更少的内存训练你的PyTorch模型?深度学习GPU内存优化策略总结
  • Linux 上离线安装 python3
  • 哪些培训课程适合学习PostgreSQL中级认证知识?
  • 前端Vue3面试题
  • blender 坐标系 金属度
  • 基于多目标向日葵优化算法(Multi-objective Sunflower Optimization,MOSFO)的移动机器人路径规划研究,MATLAB代码
  • 中科院合肥物质院迎来新一届领导班子:刘建国继续担任院长
  • 英国知名歌手批政府:让AI公司免费使用艺术家作品是盗窃
  • 因救心梗同学缺席职教高考的姜昭鹏顺利完成补考
  • 推开“房间”的门:一部“生命存在的舞台” 史
  • 阳光保险拟设立私募证券投资基金,总规模200亿元
  • 张家界一铁路致17人身亡,又有15岁女孩殒命,已开始加装护栏