文章目录
前言
此文开启一个系列,旨在秋招前将所有C++笔记进行汇总总结。
参考答案:chatgpt
一、类的封装
1.1、公有,私有,保护
类中的数据称之为数据成员,类中的函数称之为成员函数。
在C++中,类的成员变量和成员函数可以被访问控制符所限制,这些访问控制符分别是公有(public)、保护(protected)和私有(private)。举例1.1如下:
#include <iostream>class MyClass {
public:void setData(int shuru) {privateData = shuru; // 类内使用私有成员:将参数赋值给私有成员protectedData = shuru * 2; // 类内使用保护成员:将参数的两倍赋值给保护成员std::cout << "setData: privateData = " << privateData << ", protectedData = " << protectedData << std::endl;// 在类的成员函数中访问保护成员和私有成员privateFunc();protectedFunc();}int getData() {return privateData; // 类内使用私有成员::直接使用私有成员的名称}protected:int protectedData; // 保护成员void protectedFunc() {std::cout << "This is a protected function." << std::endl;} // 保护成员函数private:int privateData; // 私有成员void privateFunc() {std::cout << "This is a private function." << std::endl;} // 私有成员函数
};int main() {MyClass obj;//对象obj.setData(1234);//公有方法,可以在类外访问std::cout << "Data: " << obj.getData() << std::endl; 公有方法,可以在类外访问// obj.privateData = 456; // 私有成员不能在类的外部访问// obj.privateFunc(); // 私有函数不能在类的外部访问// obj.protectedData = 789; // 不能在类的外部访问保护成员// obj.protectedFunc(); // 保护函数不能在类的外部访问return 0;
}
1)公有成员函数可以在类的外部访问,相当于是类的外部接口。;私有成员函数和保护成员函数不能在类的外部访问。
2)私有成员和保护成员的区别在于,私有成员只能在当前类的成员函数中访问,保护成员可以在当前类的成员函数和派生类的成员函数中访问。
3)公有成员函数可以访问类的所有成员,私有成员函数和保护成员函数也可以访问类的所有成员。
1.2、类的定义和类的实现相分离
在大型的C++工程文件里,类的定义和实现经常这样分开:(下面分成了三个文件)
MyClass.h:
MyClass.h:
#ifndef MYCLASS_H
#define MYCLASS_Hclass MyClass {
public:void setData(int shuru);int getData();protected:int protectedData;void protectedFunc();private:int privateData;void privateFunc();
};#endif
如果你在一个源文件中多次使用了 #include “myheader.h”,那么这个头文件就会被多次包含。
如果一个头文件被多次包含,就会出现重复定义的问题,导致编译错误。为了避免这种问题,通常会在头文件的开头加上条件编译指令,例如 #ifndef 和 #define,来防止头文件的重复包含。这样可以保证头文件只会被包含一次,避免重复定义的问题。
MYCLASS_H 是一个头文件的命名规则,通常用于防止同一文件中同一头文件的重复包含。这个命名规则是由以下步骤组成的:
将头文件名转换为大写字母形式,例如 myclass.h 变为 MYCLASS.H。
在文件名前面加上一个下划线,例如 MYCLASS.H 变为 _MYCLASS.H。
在文件名后面加上一个宏定义,例如 _MYCLASS.H 变为 _MYCLASS_H。
这个命名规则可以有效避免同一头文件在同一文件中的重复包含,同时也可以使头文件的名字更加清晰易懂。当然,这个命名规则并不是强制性的,你可以根据自己的需求来命名头文件。
_MYCLASS.H,_MYCLASS_H等的头文件的命名规则可以有很多种,但是只要在同一项目中保持一致即可。这样可以避免同名头文件的冲突,也方便项目的维护和管理。通常情况下,团队中的成员会约定一套命名规则,然后在整个项目中统一使用。这样可以让代码更加规范化和易于维护。
MyClass.cpp:
MyClass.cpp:
#include "MyClass.h"
#include <iostream>void MyClass::setData(int shuru) {privateData = shuru; // 类内使用私有成员:将参数赋值给私有成员protectedData = shuru * 2; // 类内使用保护成员:将参数的两倍赋值给保护成员std::cout << "setData: privateData = " << privateData << ", protectedData = " << protectedData << std::endl;// 在类的成员函数中访问保护成员和私有成员privateFunc();protectedFunc();
}int MyClass::getData() {return privateData; // 类内使用私有成员::直接使用私有成员的名称
}void MyClass::protectedFunc() {std::cout << "This is a protected function." << std::endl;
} // 保护成员函数void MyClass::privateFunc() {std::cout << "This is a private function." << std::endl;
} // 私有成员函数
main.cpp:
main.cpp:
#include <iostream>
#include "MyClass.h"int main() {MyClass obj;obj.setData(1234);std::cout << "Data: " << obj.getData() << std::endl;// obj.privateData = 456; // 私有成员不能在类的外部访问// obj.privateFunc(); // 私有函数不能在类的外部访问// obj.protectedData = 789; // 不能在类的外部访问保护成员// obj.protectedFunc(); // 保护函数不能在类的外部访问return 0;
}
这样做的好处有很多:
1)使代码更加清晰和易于维护。类的界面和实现分开,可以更加清晰地区分哪些是公共接口,哪些是实现细节,方便用户和开发人员理解和使用。
2)提高代码的可重用性。将类的实现与界面分离,可以方便地将类的实现部分复用到其他项目中,而不必担心可能引入的冲突和问题。
3)增加代码的安全性。将类的实现细节隐藏起来,可以防止外部代码直接访问类的私有数据和私有方法,从而提高代码的安全性。
4)降低代码的耦合度。类的界面和实现分离,可以使类的实现部分与其他代码解耦,从而降低代码的耦合度,提高代码的可维护性和可扩展性。
在终端里可以使用下面指令执行:将MyClass.cpp文件编译成目标文件,并将其与main.cpp文件一起链接成可执行文件,才能正确运行程序。
g++ -c MyClass.cpp -o MyClass.o
g++ main.cpp MyClass.o -o main
./main.exe
当然这个算比较简单的了,复杂一些需要上Cmake
1.3、构造函数,析构函数,拷贝构造函数
构造函数和析构函数是 C++ 中特殊的成员函数,它们分别在对象构造和析构时被调用。
构造函数的主要作用是初始化对象的成员变量,为对象提供一个合理的初始状态。当我们创建一个对象时,编译器会自动调用对象的构造函数。如果我们没有定义构造函数,编译器会生成一个默认构造函数,它不做任何事情。但如果我们需要在对象创建时进行一些初始化操作,就需要定义自己的构造函数。
析构函数的主要作用是在对象销毁时释放资源,例如关闭文件、释放内存等。当对象被销毁时,编译器会自动调用对象的析构函数。如果我们没有定义析构函数,编译器会生成一个默认析构函数,它不做任何事情。但如果我们需要在对象销毁时进行一些清理操作,就需要定义自己的析构函数。
总之,构造函数和析构函数是 C++ 中非常重要的特殊成员函数,它们分别在对象构造和析构时被调用,用于初始化对象的成员变量和释放对象占用的资源。
例1.1加入构造函数和析构函数,得到例1.2,可以感受一下构造函数和析构函数的作用:
#include <iostream>
using namespace std;class MyClass {
public:MyClass(int data) : privateData(data) {protectedData = data * 2;//初始化变量cout << "MyClass constructor called." << endl;}~MyClass() {cout << "MyClass destructor called." << endl;}void setData(int shuru) {privateData = shuru;protectedData = shuru * 2;cout << "setData: privateData = " << privateData << ", protectedData = " << protectedData << endl;privateFunc();protectedFunc();}int getData() {return privateData;}protected:int protectedData;void protectedFunc() {cout << "This is a protected function." << endl;}private:int privateData;void privateFunc() {cout << "This is a private function." << endl;}
};int main() {MyClass obj(100);obj.setData(12345);cout << "Data: " << obj.getData() << endl;return 0;
}
这是一个构造函数的初始化列表,用于初始化私有成员变量privateData。privateData(data)表示将传入的参数data赋值给私有成员变量privateData。这种方式比在构造函数的函数体中直接赋值更高效,因为它避免了在构造函数体中再次对成员变量进行初始化。
当然一个类中,构造函数的形态不止一种,下面是一个例子:
#include <iostream>
using namespace std;class MyClass {
public:MyClass() : privateData(0), protectedData(0) {cout << "MyClass default constructor called." << endl;}MyClass(int data) : privateData(data) {protectedData = data * 2;cout << "MyClass constructor with one parameter called." << endl;}MyClass(int data1, int data2) : privateData(data1), protectedData(data2) {cout << "MyClass constructor with two parameters called." << endl;}~MyClass() {cout << "MyClass destructor called." << endl;}void setData(int shuru) {privateData = shuru;protectedData = shuru * 2;cout << "setData: privateData = " << privateData << ", protectedData = " << protectedData << endl;privateFunc();protectedFunc();}int getData() {return privateData;}protected:int protectedData;void protectedFunc() {cout << "This is a protected function." << endl;}private:int privateData;void privateFunc() {cout << "This is a private function." << endl;}
};int main() {MyClass obj1;MyClass obj2(100);MyClass obj3(100, 200);obj2.setData(12345);cout << "Data: " << obj2.getData() << endl;return 0;
}
这个代码中,我们添加了一个默认构造函数MyClass(),一个带有一个参数的构造函数MyClass(int data),和一个带有两个参数的构造函数MyClass(int data1, int data2)。同时,我们也修改了构造函数的输出信息,以便更好地区分它们。
输出结果:
构造函数和析构函数的关系:
构造函数和析构函数是成对出现的,构造函数用于初始化对象,而析构函数用于清理对象。在对象创建时,构造函数会被调用一次,而在对象销毁时,析构函数会被调用一次。因此,它们是一一对应的。
在程序中,构造函数和析构函数的调用次数取决于对象的创建和销毁。如果只创建了一个对象,那么构造函数和析构函数各被调用一次。如果创建了多个对象,那么每个对象都会调用一次构造函数和析构函数。如果对象是在栈上创建的,那么它们的构造函数和析构函数会在进入和离开作用域时被调用。如果对象是在堆上创建的,那么需要手动调用delete来销毁对象,这时析构函数会被调用。
需要注意的是,如果一个类继承了另一个类,那么在创建和销毁对象时,构造函数和析构函数的调用顺序是从基类到派生类的。也就是说,先调用基类的构造函数,再调用派生类的构造函数;先调用派生类的析构函数,再调用基类的析构函数。
除此之外,还有一种特殊的构造函数:拷贝构造函数:
拷贝构造函数是一种特殊的构造函数,用于将一个对象的值复制到另一个对象中。拷贝构造函数的作用是创建一个新对象,并将已有对象的值复制到新对象中。它常用于以下三种情况:
1)用一个已有对象来初始化一个新对象。
2)将一个对象作为参数传递给一个函数。
3)在函数中返回一个对象。
举个例子:
#include <iostream>
using namespace std;class MyClass {
public:MyClass() : privateData(0), protectedData(0) {cout << "MyClass default constructor called." << endl;}MyClass(int data) : privateData(data) {protectedData = data * 2;cout << "MyClass constructor with one parameter called." << endl;}MyClass(int data1, int data2) : privateData(data1), protectedData(data2) {cout << "MyClass constructor with two parameters called." << endl;}MyClass(const MyClass& other) {privateData = other.privateData;protectedData = other.protectedData;cout << "MyClass copy constructor called." << endl;}~MyClass() {cout << "MyClass destructor called." << endl;}void setData(int shuru) {privateData = shuru;protectedData = shuru * 2;cout << "setData: privateData = " << privateData << ", protectedData = " << protectedData << endl;privateFunc();protectedFunc();}int getData() {return privateData;}protected:int protectedData;void protectedFunc() {cout << "This is a protected function." << endl;}private:int privateData;void privateFunc() {cout << "This is a private function." << endl;}
};void func1(MyClass obj) {cout << "func1 called." << endl;
}MyClass func2() {cout << "func2 called." << endl;MyClass obj(456);return obj;
}int main() {MyClass obj1;MyClass obj2(100);MyClass obj3(100, 200);obj2.setData(12345);cout << "Data: " << obj2.getData() << endl;MyClass obj4(obj2);MyClass obj5 = obj3;func1(obj4);MyClass obj6 = func2();return 0;
}
这里添加了一个拷贝构造函数,用于将一个对象的值复制到另一个对象中。同时,我添加了两个函数,一个是将对象作为参数传递给函数,另一个是在函数中返回对象。这两个函数都会自动调用拷贝构造函数。
在 main 函数中,我创建了几个对象,并调用了它们的成员函数。然后,我创建了两个新的对象,一个是通过拷贝构造函数创建的,另一个是通过赋值运算符创建的。最后,我调用了两个函数,一个是将对象作为参数传递给函数,另一个是在函数中返回对象。在这两个函数中,都会自动调用拷贝构造函数。
在这些情况下,如果没有定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数来完成对象的复制。如果需要进行深拷贝,需要自己定义拷贝构造函数。
1.4、静态数据成员和静态成员函数
静态数据成员是指属于类的成员,而不是属于类的对象的成员。它们是在类被定义时被声明的,而不是在类的对象被创建时被定义的。它们被所有该类的对象所共享,因此它们的值在所有对象之间都是相同的。静态数据成员在程序开始时候被创建,在程序结束时被销毁(而不是构造和析构)
静态数据成员的存在有以下几个好处:
与普通成员变量不同,静态数据成员不需要在每个对象中都存储一份。这可以节省内存空间。
静态数据成员可以被所有该类的对象所共享,这使得它们可以用于在类的所有对象之间传递信息。
静态数据成员可以在类的外部被访问,这使得它们可以用于实现类似于全局变量的功能。
静态数据成员可以被用作常量表达式,这使得它们可以用于在编译时计算常量值。
静态成员函数是指在类中使用 static 关键字修饰的成员函数。静态成员函数不依赖于任何对象,可以直接通过类名调用,而不需要创建对象。因此,静态成员函数不能访问非静态成员变量和非静态成员函数,只能访问静态成员变量和静态成员函数。
(示例):
#include <iostream>
using namespace std;class MyClass {
public:MyClass() : privateData(0), protectedData(0) {cout << "MyClass default constructor called." << endl;count++; // 每次创建对象时,对象数量加1}MyClass(int data) : privateData(data) {protectedData = data * 2;cout << "MyClass constructor with one parameter called." << endl;count++; // 每次创建对象时,对象数量加1}MyClass(int data1, int data2) : privateData(data1), protectedData(data2) {cout << "MyClass constructor with two parameters called." << endl;count++; // 每次创建对象时,对象数量加1}MyClass(const MyClass& other) {privateData = other.privateData;protectedData = other.protectedData;cout << "MyClass copy constructor called." << endl;count++; // 每次创建对象时,对象数量加1}~MyClass() {cout << "MyClass destructor called." << endl;count--; // 每次销毁对象时,对象数量减1}void setData(int shuru) {privateData = shuru;protectedData = shuru * 2;cout << "setData: privateData = " << privateData << ", protectedData = " << protectedData << endl;privateFunc();protectedFunc();}int getData() {return privateData;}static int getCount() {return count; // 返回对象数量}protected:int protectedData;void protectedFunc() {cout << "This is a protected function." << endl;}private:int privateData;void privateFunc() {cout << "This is a private function." << endl;}static int count; // 所有对象的数量
};int MyClass::count = 0; // 在类外部初始化静态数据成员void func1(MyClass obj) {cout << "func1 called." << endl;
}MyClass func2() {cout << "func2 called." << endl;MyClass obj(456);return obj;
}int main() {MyClass obj1;MyClass obj2(100);MyClass obj3(100, 200);obj2.setData(12345);cout << "Data: " << obj2.getData() << endl;MyClass obj4(obj2);MyClass obj5 = obj3;cout << "Total objects: " << MyClass::getCount() << endl; // 输出对象数量func1(obj4);MyClass obj6 = func2();cout << "Total objects: " << MyClass::getCount() << endl; // 输出对象数量return 0;
}
静态成员经常用来做计数器的功能,就像上面代码所示。
当然静态数据成员可以被对象引用,比如MyClass::getCount()其实和obj6.getCount()实现的效果差不多,但是:
MyClass::getCount() 是静态调用,不需要创建对象,可以在任何地方调用;
obj6.getCount() 是动态调用,需要先创建对象,然后通过对象名调用。
另外需要注意的是,静态成员函数只能访问静态成员变量和静态成员函数,而不能访问非静态成员变量和非静态成员函数。因此,如果需要访问非静态成员变量或非静态成员函数,就需要通过对象名调用成员函数。
1.5、友元函数,友元类
友元是C++语言中的一个特殊概念,它允许某个函数或类访问另一个类的保护成员和私有成员。C++中的封装性要求类的私有成员和保护成员只能被类的成员函数访问,而友元机制则打破了这个限制,允许某些外部函数或类访问私有成员和保护成员,从而提高了程序的灵活性和可扩展性。
友元函数可以放在类中的任意位置,实现部分放在类外。
友元类是指一个类可以访问另一个类的私有成员和保护成员,这个类就是另一个类的友元类。友元类的声明方式和友元函数类似,需要在类的声明中使用friend关键字进行声明。
友元类可以访问被它所声明的类的私有成员和保护成员,但是它本身并不是被它所声明的类的成员,因此它不能直接访问被它所声明的类的成员变量和成员函数。如果需要访问被它所声明的类的成员变量和成员函数,可以通过类对象或者指针来访问。
下面是一个例子:
#include <iostream>
using namespace std;class MyClass {
public:MyClass() : privateData(0), protectedData(0) {cout << "MyClass default constructor called." << endl;count++; // 每次创建对象时,对象数量加1}MyClass(int data) : privateData(data) {protectedData = data * 2;cout << "MyClass constructor with one parameter called." << endl;count++; // 每次创建对象时,对象数量加1}MyClass(int data1, int data2) : privateData(data1), protectedData(data2) {cout << "MyClass constructor with two parameters called." << endl;count++; // 每次创建对象时,对象数量加1}MyClass(const MyClass& other) {privateData = other.privateData;protectedData = other.protectedData;cout << "MyClass copy constructor called." << endl;count++; // 每次创建对象时,对象数量加1}~MyClass() {cout << "MyClass destructor called." << endl;count--; // 每次销毁对象时,对象数量减1}void setData(int shuru) {privateData = shuru;protectedData = shuru * 2;cout << "setData: privateData = " << privateData << ", protectedData = " << protectedData << endl;privateFunc();protectedFunc();}int getData() {return privateData;}static int getCount() {return count; // 返回对象数量}protected:int protectedData;void protectedFunc() {cout << "This is a protected function." << endl;}private:int privateData;void privateFunc() {cout << "This is a private function." << endl;}static int count; // 所有对象的数量friend void friendFunc(MyClass obj); // 声明友元函数friend class FriendClass; // 声明友元类,FriendClass是Myclass的友元类
};int MyClass::count = 0; // 在类外部初始化静态数据成员void friendFunc(MyClass obj) { // 定义友元函数cout << "friendFunc called, privateData = " << obj.privateData << endl;obj.privateFunc();
}class FriendClass { // 定义友元类
public:void friendClassFunc(MyClass obj) {cout << "FriendClass called, protectedData = " << obj.protectedData << endl;obj.protectedFunc();}
};void func1(MyClass obj) {cout << "func1 called." << endl;friendFunc(obj); // 调用友元函数
}
MyClass func2() {cout << "func2 called." << endl;MyClass obj(456);return obj;
}
int main() {MyClass obj1;MyClass obj2(100);MyClass obj3(100, 200);obj2.setData(12345);cout << "Data: " << obj2.getData() << endl;MyClass obj4(obj2);MyClass obj5 = obj3;cout << "Total objects: " << MyClass::getCount() << endl; // 输出对象数量func1(obj4);FriendClass friendObj; // 创建友元类对象friendObj.friendClassFunc(obj2); // 调用友元类的成员函数MyClass obj6 = func2();cout << "Total objects: " << MyClass::getCount() << endl; // 输出对象数量return 0;}
友元的弊端主要有以下几点:
破坏了封装性:友元可以访问类的私有成员和保护成员,这破坏了封装性,可能会导致类的安全性降低。
可能会导致耦合度增加:友元函数或友元类需要知道类的实现细节,这可能会导致类和友元之间的耦合度增加,使得类的实现变得更加复杂。
友元不具有继承性:如果一个类是另一个类的友元,那么它并不具有继承性,即它不能访问派生类的私有成员和保护成员。
因此,在使用友元时需要慎重考虑,只有在必要的情况下才应该使用友元。
二、类的实现——对象
2.1、对象的静态分配,动态分配(堆对象)
对象内存的静态分配和动态分配是指在程序运行时,对象所占用的内存空间是在编译时确定还是在运行时动态分配的。
静态分配是指在编译时为对象分配固定大小的内存空间,这种方式也称为栈分配。栈是一种先进后出的数据结构,它的内存分配和释放是由系统自动完成的,程序员不需要手动管理内存。静态分配的优点是速度快,缺点是内存空间固定,不够灵活,无法动态扩展。
动态分配是指在程序运行时根据需要动态地分配内存空间,这种方式也称为堆分配。堆是一种动态分配内存的方式,程序员需要手动管理内存的分配和释放。动态分配的优点是灵活性强,可以动态地扩展内存空间,缺点是速度较慢,容易出现内存泄漏和内存碎片等问题。
对于对象的内存分配,一般情况下建议使用动态分配,尤其是对象的大小不确定或者需要动态扩展时。但是在某些情况下,静态分配也是一种不错的选择,例如对象的大小固定且较小,或者需要频繁创建和销毁对象时,使用静态分配可以提高程序的运行效率。
静态分配的对象也称为自动变量(automatic variable)、栈变量(stack variable)或局部变量(local variable)。它们的内存在程序编译时就已经分配好了,作用域只在当前代码块中,当代码块执行完毕时,它们会自动被销毁。
动态分配的对象也称为堆对象(heap object)、动态对象(dynamic object)或者动态内存分配对象(dynamically allocated object)。它们的内存空间在程序运行时动态分配,作用域可以跨越多个代码块,需要手动管理内存的分配和释放。
下面是堆对象建立删除,堆对象数组建立删除的例子
#include <iostream>
using namespace std;class MyClass
{
public:MyClass() { cout << "MyClass constructed!" << endl; }~MyClass() { cout << "MyClass destructed!" << endl; }
};int main()
{// 建立堆对象MyClass* ptr = new MyClass();// 使用堆对象cout << "Using heap object..." << endl;// 删除堆对象delete ptr;return 0;
}
#include <iostream>
using namespace std;class MyClass
{
public:MyClass() { cout << "MyClass constructed!" << endl; }~MyClass() { cout << "MyClass destructed!" << endl; }
};int main()
{// 建立堆对象数组MyClass* arr = new MyClass[5];// 使用堆对象数组cout << "Using heap object array..." << endl;// 删除堆对象数组delete[] arr;return 0;
}
2.2、子对象
2.3、this指针
this指针是一个指向当前对象的指针,它在成员函数中使用。this指针的作用是为了区分同名的成员变量和局部变量。在成员函数中,如果有一个参数与成员变量同名,那么在函数中直接使用该变量名时,编译器会默认使用参数而不是成员变量。这时可以使用this指针来明确指出要使用的是成员变量而不是参数。
另外,this指针还可以用于在一个成员函数中返回当前对象的引用。例如,可以在一个成员函数中返回*this,表示返回当前对象的引用。这在链式调用中非常常见,可以使代码更加简洁易读。
举个例子:
#include <iostream>
using namespace std;class Person {
public:int age;int getAge() {return this->age;}
};int main() {Person person1;person1.age = 18;cout << "person1's age is " << person1.getAge() << endl;Person person2;person2.age = 25;cout << "person2's age is " << person2.getAge() << endl;return 0;
}
上面等价于下面:
```cpp
#include <iostream>
using namespace std;class Person {
public:int age;int getAge() {return age;}
};int main() {Person person1;person1.age = 18;cout << "person1's age is " << person1.getAge() << endl;Person person2;person2.age = 25;cout << "person2's age is " << person2.getAge() << endl;return 0;
}
在成员函数中,可以直接访问类的成员变量,不需要使用"this"指针。因此,"return this->age"和"return age"是等价的。不过,使用"this"指针可以明确地表示当前对象,增加代码的可读性。