宜昌市建设工程质量监督站网站推广平台都有哪些

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

宜昌市建设工程质量监督站网站,推广平台都有哪些,封面新闻是国家级媒体,四川大学官方网站规划建设处《C程序设计基础教程》——刘厚泉#xff0c;李政伟#xff0c;二零一三年九月版#xff0c;学习笔记 文章目录 1、类的定义1.1、结构体和类1.2、基本概念1.3、成员函数的定义1.4、内联成员函数 2、对象2.1、对象的定义2.2、成员访问 3、构造函数3.1、构造函数的定义3.2、子… 《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 ProgrammingOOP的场景如支持多态、继承、虚函数等复杂的设计模式。 结构体通常用于定义较为简单的数据结构重点在于数据的存储而不涉及太多的行为。结构体更适合用于组织和存储简单数据特别是数据结构和轻量级对象。 语义上的差异 尽管在技术上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){Namename;Phiphi;Mathmath;}void Display(){cout Name \t Phi \t Math \t Ave endl;}void Average(){Ave(PhiMath)/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.hour8; // error, 私有成员无法访问// time.minute10; // error, 私有成员无法访问time.set_time();time.show_time();return 0; }output 12 13 14 12:13:141.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){ // 定义成员函数向数据成员赋值hourh;minutem;secs;} 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:152在类体外定义成员函数 返回类型 类名::成员函数名(参数说明)这个时候要用作用域运算符 :: 来指定成员函数属于哪个类 #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) { // 定义成员函数向数据成员赋值hourh;minutem;secs; } 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都是内联函数{ // 定义成员函数向数据成员赋值hourh;minutem;secs;} 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、成员访问 一个对象的成员就是该对象的类所定义的成员包括数据成员和成员函数。 类作用域又称类域在类域中定义的数据成员不能使用 auto、register 和 extern 等修饰符只能用 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都是内联函数{ // 定义成员函数向数据成员赋值hourh;minutem;secs;} 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:153、构造函数 3.1、构造函数的定义 构造Constructor函数在对象被创建时由编译系统自动调用用于完成成员的初始化工作。 构造函数是一种特殊的成员函数它有如下特性 名称与类名相同构造函数的名称必须与类名完全一致以便编译器能够识别。无返回类型构造函数没有返回类型包括 void 类型。自动调用在创建对象时构造函数会自动被调用且只调用一次。支持重载一个类可以有多个构造函数通过参数个数、类型或顺序等进行重载以满足不同的初始化需求。 构造函数最好是 public 的private 构造函数不能直接用来初始化对象。 构造函数的种类 1默认构造函数 定义无参数的构造函数。 特点如果类中没有显式定义构造函数编译器会自动生成一个默认的无参构造函数该构造函数不做任何工作。如果定义了其他构造函数编译器则不再自动生成默认构造函数。 使用场景适用于不需要初始化参数的情况或者为成员变量提供默认值。 C规定每个类必须至少有一个构造函数没有显示定义系统会提供一个无参数的构造函数缺省的构造函数 2带参数的构造函数 定义接受一个或多个参数的构造函数。 特点在创建对象时可以传入参数来初始化成员变量。 使用场景适用于需要在创建对象时初始化成员变量的情况。 3拷贝构造函数 定义用于复制一个已有对象来初始化新对象的构造函数。 特点参数为同类对象的引用。如果类中没有显式定义拷贝构造函数编译器会自动生成一个默认的拷贝构造函数执行浅拷贝。 使用场景当需要用一个对象来初始化另一个同类对象时如对象赋值、函数参数传递、函数返回值等。 4移动构造函数C11及以后 定义允许资源的所有权从一个对象转移到另一个对象的构造函数。 特点通过右值引用实现主要用于优化临时对象的资源管理。 使用场景适用于处理临时对象提高资源管理的效率。 5委托构造函数C11及以后 定义允许一个构造函数调用同一个类的另一个构造函数的构造函数。 特点可以减少代码重复提高代码的可读性和可维护性。 使用场景当类有多个构造函数且它们之间有共同的初始化代码时可以使用委托构造函数来避免代码重复。 eg8-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都是内联函数{ // 定义成员函数向数据成员赋值hourh;minutem;secs;} 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:15eg 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 h15, int w15, int l15) {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){nTestn;}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) // 构造函数{nTestn;}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智能指针 在 C11 及之后的标准中引入了智能指针如 std::unique_ptr 和 std::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; // 学生人数1sumScoreScore; // 总分数增加cout SName Constructor called. endl; }CStudent::~CStudent() {studentTotal–; // 学生人数-1sumScore-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; // 学生人数1sumScoreScore; // 总分数增加cout SName Constructor called. endl; }CStudent::~CStudent() {studentTotal–; // 学生人数-1sumScore-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 iostreamclass 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: 205.2、静态成员函数 静态成员函数是属于类本身而不是某个特定对象的成员函数。与静态数据成员类似静态成员函数可以在没有对象的情况下调用。它们主要用于处理与类相关的操作而不是与特定对象相关的操作。 静态成员函数没有 this 指针通常它只访问属于全体对象的成员——静态成员也可以访问全局变量 一般情况下静态成员函数不访问类的非静态成员因为这些非静态成员是属于特定对象的。 eg 8-21 中类 CStudent 的静态成员函数 static float average() 定义如下 float CStudent::average() {return (sumScore / studentTotal);}使用了静态数据成成员 sumScore 和 studentTotal 如果将 void Print(); 成员函数声明为静态成员函数 static void Print();编译会出现错误因为在函数体中的语句 cout SName : Score endl; 中使用了非静态数据成员 SName 和 Score 特性 属于类本身 静态成员函数属于类而不是类的对象。它们可以在没有创建对象的情况下通过类名直接调用。 不能访问非静态成员 静态成员函数不能访问类的非静态成员包括非静态数据成员和非静态成员函数因为它们没有 this 指针。但是静态成员函数可以访问静态数据成员和其他静态成员函数。注意非静态成员函数可以任意地访问静态成员函数和静态数据成员。 共享性 静态成员函数在所有对象中共享但它们不依赖于任何对象。 语法 在类内声明静态成员函数时使用关键字 static。出现在类体外的静态成员函数定义不能指定关键字 static。调用静态成员函数时可以使用类名或对象来调用但通常使用类名。 静态成员函数通常用于以下场景 工厂方法 用于创建对象实例而不需要依赖对象本身。 实用函数 提供与类相关的实用功能例如处理所有对象的统计信息。 访问静态成员 用于访问和修改静态数据成员。 5.3、静态成员的访问 类的公有静态数据成员既可以用类的对象访问也可以直接用作用域运算符 :: 通过类名来访问 静态数据成员 类名::静态数据成员名 // 建议使用此形式或 对象名.静态数据成员名 // 不建议使人误以为静态数据成员是属于某个对象的类的公有静态成员函数访问和公有静态数据成员一样有两种形式 静态成员函数 类名::静态成员函数名 // 建议使用此形式或 对象名.静态成员函数名 // 不建议使用例子参考上述的 eg 8-21 eg #include iostreamclass 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: 206、应用实例 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拷贝构造函数自己定义的好处在于可以自行决定要拷贝的数据成员 构造函数可以有多个 注意拷贝的形式直接 AB 或者 A(B) eg 8-23 设计一个复数类进行复数的运算。并能实现运算结果的输出。 #include iostream using namespace std;class Complex {private:double Real;double Image;public:Complex(double r0, double i0) // 带有默认值的构造函数{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();cc1.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 i0; i4; i)toy[i].Compute();for(int i0; i4; 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编程中了解堆和栈的区别以及它们的使用场景对于编写高效、安全的代码至关重要。栈适用于存储小型、生命周期短的数据而堆则适用于存储大型、生命周期长的数据。在实际编程中应根据具体需求选择合适的内存分配方式并注意避免常见的内存管理错误。