第七章 类_Part1
1.类和对象
1.1类和对象的基本概念
1.1.1C和C++中struct区别
- c语言struct只有变量
- c++语言struct 既有变量,也有函数
1.1.2 类的封装
我们编写程序的目的是为了解决现实中的问题,而这些问题的构成都是由各种事物组成,我们在计算机中要解决这种问题,首先要做就是要将这个问题的参与者:事和物抽象到计算机程序中,也就是用程序语言表示现实的事物。
那么现在问题是如何用程序语言来表示现实事物?现实世界的事物所具有的共性就是每个事物都具有自身的属性,一些自身具有的行为,所以如果我们能把事物的属性和行为表示出来,那么就可以抽象出来这个事物。
比如我们要表示人这个对象,在c语言中,我们可以这么表示:
typedef struct _Person{ char name[64]; int age; }Person; typedef struct _Aninal{ char name[64]; int age; int type; //动物种类 }Ainmal; void PersonEat(Person* person){ printf("%s在吃人吃的饭!\n",person->name); } void AnimalEat(Ainmal* animal){ printf("%s在吃动物吃的饭!\n", animal->name); } int main(){ Person person; strcpy(person.name, "小明"); person.age = 30; AnimalEat(&person); return EXIT_SUCCESS; }
定义一个结构体用来表示一个对象所包含的属性,函数用来表示一个对象所具有的行为,这样我们就表示出来一个事物,在c语言中,行为和属性是分开的,也就是说吃饭这个属性不属于某类对象,而属于所有的共同的数据,所以不单单是PeopleEat可以调用Person数据,AnimalEat也可以调用Person数据,那么万一调用错误,将会导致问题发生。
从这个案例我们应该可以体会到,属性和行为应该放在一起,一起表示一个具有属性和行为的对象。
假如某对象的某项属性不想被外界获知,比如说漂亮女孩的年龄不想被其他人知道,那么年龄这条属性应该作为女孩自己知道的属性;或者女孩的某些行为不想让外界知道,只需要自己知道就可以。那么这种情况下,封装应该再提供一种机制能够给属性和行为的访问权限控制住。
所以说封装特性包含两个方面,一个是属性和变量合成一个整体,一个是给属性和函数增加访问权限。
- 封装(Encapsulation)
- 把变量(属性)和函数(操作)合成一个整体,封装在一个类中
- 对变量和函数进行访问控制
- 访问权限
- 在类的内部(作用域范围内),没有访问权限之分,所有成员可以相互访问
- 在类的外部(作用域范围外),访问权限才有意义:public,private,protected
- 在类的外部,只有public修饰的成员才能被访问,在没有涉及继承与派生时,private和protected是同等级的,外部不允许访问
protected:子类可以访问
private:子类不可以访问
//封装两层含义 //1. 属性和行为合成一个整体 //2. 访问控制,现实事物本身有些属性和行为是不对外开放 class Person{ //人具有的行为(函数) public: void Dese(){ cout << "我有钱,年轻,个子又高,就爱嘚瑟!" << endl;} //人的属性(变量) public: int mTall; //多高,可以让外人知道 protected: int mMoney; // 有多少钱,只能儿子孙子知道 private: int mAge; //年龄,不想让外人知道 }; int main(){ Person p; p.mTall = 220; //p.mMoney 保护成员外部无法访问 //p.mAge 私有成员外部无法访问 p.Dese(); return EXIT_SUCCESS; }
**[struct和class的区别?] **
struct默认访问权限为public
class默认访问权限为private
class A{ int mAge; }; struct B{ int mAge; }; void test(){ A a; B b; //a.mAge; //无法访问私有成员 b.mAge; //可正常外部访问 }
4.1.3 将成员变量设置为private
-
可赋予客户端访问数据的一致性。
如果成员变量不是public,客户端唯一能够访问对象的方法就是通过成员函数。如果类中所有public权限的成员都是函数,客户在访问类成员时只会默认访问函数,不需要考虑访问的成员需不需要添加(),这就省下了许多搔首弄耳的时间。
-
可细微划分访问控制。
使用成员函数可使得我们对变量的控制处理更加精细。如果我们让所有的成员变量为public,每个人都可以读写它。如果我们设置为private,我们可以实现“不准访问”、“只读访问”、“读写访问”,甚至你可以写出“只写访问”。
class AccessLevels{ public: //对只读属性进行只读访问 int getReadOnly(){ return readOnly; } //对读写属性进行读写访问 void setReadWrite(int val){ readWrite = val; } int getReadWrite(){ return readWrite; } //对只写属性进行只写访问 void setWriteOnly(int val){ writeOnly = val; } private: int readOnly; //对外只读访问 int noAccess; //外部不可访问 int readWrite; //读写访问 int writeOnly; //只写访问 };
代码示例:
#include<iostream> #include<string> using namespace std; class Person { public: void setAge(int age) { if (age < 0 || age > 100) { cout << "呔,妖精" << endl; return; } m_Age = age; } //获取年龄,读权限 int getAge() { return m_Age; } //获取姓名,读权限 string getName() { return m_Name; } //写姓名 void setName(string name) { m_Name = name; } //只写的权限 void setLover(string lover) { m_lover = lover; } private: int m_Age = 0;//年龄,只读 string m_Name;//读写 string m_lover;// }; void test01() { Person p1; p1.setName("老王"); p1.setLover("马冬梅"); //为啥先输出的是呔,妖精?不应该是 p1的年龄是老王 吗, //因为之前p1.setAger()放前面了 cout << "p1的姓名:" << p1.getName() << endl; p1.setAge(180); cout << "p1的年龄:" << p1.getAge() << endl; } int main() { test01(); system("pause"); return 0; }
4.2 对象的构造和析构
对象的初始化和清理也是两个非常重要的安全问题,一个对象或者变量没有初始时,对其使用后果是未知,同样的使用完一个变量,没有及时清理,也会造成一定的安全问题。c++为了给我们提供这种问题的解决方案,构造函数和析构函数,这两个函数将会被编译器自动调用,完成对象初始化和对象清理工作。
无论你是否喜欢,对象的初始化和清理工作是编译器强制我们要做的事情,即使你不提供初始化操作和清理操作,编译器也会给你增加默认的操作,只是这个默认初始化操作不会做任何事,所以编写类就应该顺便提供初始化函数。
为什么初始化操作是自动调用而不是手动调用?既然是必须操作,那么自动调用会更好,如果靠程序员自觉,那么就会存在遗漏初始化的情况出现。
4.2.1 构造函数和析构函数(作用域都是public)
构造函数和析构函数,这两个函数将会被编译器自动调用。
构造函数主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
构造函数语法:
构造函数函数名和类名相同,没有返回值,不能有void,但可以有参数,可以发生重载。
只能调用一次
ClassName(){}
析构函数主要用于对象销毁前系统自动调用,执行一些清理工作。
析构函数语法:
析构函数函数名是在类名前面加”~”组成,没有返回值,不能有void,不能有参数,不能重载。
~ClassName(){}
代码示例:
点击查看代码
#define _CRT_SECURE_NO_WARNINGS #include<iostream> #include<string> using namespace std; class Person { public: Person() { cout << "构造函数调用: " << endl; pName = (char *)malloc(sizeof("John")); strcpy(pName, "John"); mTall = 150; mMonkey = 100; } ~Person() { cout << "析构函数调用:" << endl; if (pName != NULL) { free(pName); pName = NULL; } } public: char *pName; int mTall; int mMonkey; }; void test() { Person person; cout << person.pName << endl; cout << person.mMonkey << endl; } int main() { test(); system("pause"); return 0; }
4.3.1 构造函数的分类及调用
1.构造函数分类:
- 按参数类型:分为无参构造函数和有参构造函数
- 按类型分类:普通构造函数和拷贝构造函数(复制构造函数)
2、构造函数的调用规则:
-
编译器会给一个类,至少添加三个函数 :
默认构造(无参,函数体为空)
析构函数(无参,函数体为空)
拷贝构造(对类中非静态成员属性简单值拷贝) -
如果我们自己提供了有参构造函数,编译器就不会提供默认构造函数,但是依旧会提供默认的拷贝构造函数,进行简单的值拷贝
-
如果我们自己提供了拷贝构造函数,编译器就不会提供其他任何默认构造函数。
-
析构函数是一定会提供的
class Person{ public: Person(){//1.无参构造函数 cout << "no param constructor!" << endl; mAge = 0; } //2.有参构造函数 Person(int age){ cout << "1 param constructor!" << endl; mAge = age; } //3.拷贝构造函数(复制构造函数) 使用另一个对象初始化本对象 Person(const Person& person){ cout << "copy constructor!" << endl; mAge = person.mAge; } //打印年龄 void PrintPerson(){ cout << "Age:" << mAge << endl; } private: int mAge; }; //1. 无参构造调用方式 void test01(){ //调用无参构造函数 Person person1; //默认构造函数不要加(),不然编译器会误认为是函数声明。 person1.PrintPerson(); //无参构造函数错误调用方式 //Person person2(); //person2.PrintPerson(); } //2. 调用有参构造函数 void test02(){ //第一种 括号法,最常用 Person person01(100); person01.PrintPerson(); //调用拷贝构造函数 Person person02(person01);//或者Person p1 = Person(p2); person02.PrintPerson(); //第二种 匿名对象(显示调用构造函数) Person(200); //匿名对象,没有名字的对象 //如果编译器发现了对象是匿名的,那么在这行代码结束后就释放这个对象。 //cout << "aaaa" << endl;//验证匿名对象释放时机 Person person03 = Person(300); person03.PrintPerson(); //不能用拷贝构造函数初始化匿名对象 //Person(p5);//如果这么写,编译器认为你写成了 Person p5,对象的声明; //而如果写成右值,那么可以 Person p6 = Person(p5); //注意: 使用匿名对象初始化判断调用哪一个构造函数,要看匿名对象的参数类型 Person person06(Person(400)); //等价于 Person person06 = Person(400); person06.PrintPerson(); //第三种 =号法 隐式转换 Person person04 = 100; //Person person04 = Person(100) person04.PrintPerson(); //调用拷贝构造 Person person05 = person04; //Person person05 = Person(person04) person05.PrintPerson(); } int main() { test01(); test02(); system("pause"); return 0; }
b为A的实例化对象,A a = A(b) 和 A(b)的区别?
当A(b) 有变量来接的时候,那么编译器认为他是一个匿名对象,当没有变量来接的时候,编译器认为你A(b) 等价于 A b。
点击查看代码
#define _CRT_SECURE_NO_WARNINGS #include<iostream> using namespace std; class MyClass { public: /*MyClass() { cout << "默认构造函数" << endl; }*/ MyClass(int a) { cout << "有参构造函数" << endl; } int m_A; }; class MyClass2 { MyClass2(const MyClass &a) { cout << "拷贝构造函数" << endl; } int m_A; }; void test01() { //MyClass c1;//这个调用默认构造的就调用不了了,要想调用默认的构造函数,需要将其先写出来 MyClass c1(1);//调用有参构造 c1.m_A = 10; MyClass c2(c1);//调用拷贝构造 cout << c2.m_A << endl; } //2.如果用户定义拷贝构造函数,c++不会再提供任何默认构造函数 void test02() { //MyClass2 c1;//调用拷贝构造//error:不存在默认的构造函数;那么,此时要创建对象应该咋办 } int main() { test01(); system("pause"); return 0; } //析构函数是一定会提供的
4.3.2 拷贝构造函数的调用时机
- 把对象以值传递的方式传给函数参数
- 函数局部对象以值传递的方式从函数返回(vs debug模式下调用一次拷贝构造,qt不调用任何构造)
- 用一个对象初始化另一个对象
class Person{ public: Person(){ cout << "no param contructor!" << endl; mAge = 10; } Person(int age){ cout << "param constructor!" << endl; mAge = age; } Person(const Person& person){ cout << "copy constructor!" << endl; mAge = person.mAge; } ~Person(){ cout << "destructor!" << endl; } public: int mAge; }; //1. 旧对象初始化新对象 void test01(){ Person p(10);//调用有参构造函数,创建对象 Person p1(p);//用已经创建的对象,调用拷贝构造函数 //初始化新的对象 Person p2 = Person(p); Person p3 = p; // 相当于Person p2 = Person(p); }  //2. 传递的参数是普通对象,函数参数也是普通对象,传递将会调用拷贝构造 //值传递都会开辟一块内存,而引用传递则不会 void doBussiness(Person p){}//Person p = Person(p1); void test02(){ Person p1(10); doBussiness(p1); }  //3. 函数返回局部对象 Person MyBusiness(){ Person p(10); cout << "局部p:" << (int*)&p << endl; return p; } void test03(){ //vs release、qt下没有调用拷贝构造函数 //vs debug下调用一次拷贝构造函数 Person p = MyBusiness(); cout << "局部p:" << (int*)&p << endl; }
*> [Test03结果说明:]
编译器存在一种对返回值的优化技术,RVO(Return Value Optimization).在vs debug模式下并没有进行这种优化,所以函数MyBusiness中创建p对象,调用了一次构造函数,当编译器发现你要返回这个局部的对象时,编译器通过调用拷贝构造生成一个临时Person对象返回,然后调用p的析构函数。
我们从常理来分析的话,这个匿名对象和这个局部的p对象是相同的两个对象,那么如果能直接返回p对象,就会省去一个拷贝构造和一个析构函数的开销,在程序中一个对象的拷贝也是非常耗时的,如果减少这种拷贝和析构的次数,那么从另一个角度来说,也是编译器对程序执行效率上进行了优化。
所以在这里,编译器偷偷帮我们做了一层优化:
当我们这样去调用: Person p = MyBusiness();
编译器偷偷将我们的代码更改为:
void MyBussiness(Person& _result){ _result.X:X(); //调用Person默认拷贝构造函数 //.....对_result进行处理 return; } int main(){ Person p; //这里只分配空间,不初始化 MyBussiness(p); } ```***
4.3.4 多个对象构造和析构
4.3.4.1 初始化列表
构造函数和其他函数不同,除了有名字,参数列表,函数体之外还有初始化列表。
初始化列表简单使用:
#if 0 是对中间这段代码的屏蔽,当#if 0修改成#if 1的时候,中间这段代码就能使用了 #endif
注意:初始化成员列表(参数列表)只能在构造函数使用。
class Person{ public: #if 0 //传统方式初始化 Person(int a,int b,int c){ mA = a; mB = b; mC = c; } #endif //初始化列表方式初始化 Person(int a, int b, int c):mA(a),mB(b),mC(c){} void PrintPerson(){ cout << "mA:" << mA << endl; cout << "mB:" << mB << endl; cout << "mC:" << mC << endl; } private: int mA; int mB; int mC; };
点击查看代码
#define _CRT_SECURE_NO_WARNINGS #include<iostream> using namespace std; class MyClass { public: /*MyClass() { cout << "默认构造函数" << endl; }*/ MyClass(int a) { cout << "有参构造函数" << endl; } int m_A; }; class MyClass2 { MyClass2(const MyClass &a) { cout << "拷贝构造函数" << endl; } int m_A; }; void test01() { //MyClass c1;//这个调用默认构造的就调用不了了,要想调用默认的构造函数,需要将其先写出来 MyClass c1(1);//调用有参构造 c1.m_A = 10; MyClass c2(c1);//调用拷贝构造 cout << c2.m_A << endl; } //2.如果用户定义拷贝构造函数,c++不会再提供任何默认构造函数 void test02() { //MyClass2 c1;//调用拷贝构造//error:不存在默认的构造函数;那么,此时要创建对象应该咋办 } int main() { test01(); system("pause"); return 0; } //析构函数是一定会提供的
4.3.4 多个对象构造和析构
4.3.4.1 初始化列表
构造函数和其他函数不同,除了有名字,参数列表,函数体之外还有初始化列表。
初始化列表简单使用:
#if 0 是对中间这段代码的屏蔽,当#if 0修改成#if 1的时候,中间这段代码就能使用了 #endif
注意:初始化成员列表(参数列表)只能在构造函数使用。
class Person{ public: #if 0 //传统方式初始化 Person(int a,int b,int c){ mA = a; mB = b; mC = c; } #endif //初始化列表方式初始化 Person(int a, int b, int c):mA(a),mB(b),mC(c){} void PrintPerson(){ cout << "mA:" << mA << endl; cout << "mB:" << mB << endl; cout << "mC:" << mC << endl; } private: int mA; int mB; int mC; };
代码示例:
#define _CRT_SECURE_NO_WARNINGS #include<iostream> using namespace std; class Person { public: //Person(){} //有参构造和拷贝构造都是初始化数据用的 /*Person(int a,int b,int c) { m_A = a; m_B = b; m_C = c; }*/ //默认构造,写死了的 Person() :m_A(10), m_B(20), m_C(30){} //利用初始化列表来初始化数据 //形式:构造函数后面 + 属性(参数) + 属性(参数) + ...{} Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) {} int m_A; int m_B; int m_C; }; void test01() { Person p1(100, 200, 300); cout << "p1的m_A: " << p1.m_A << endl; cout << "p1的m_B: " << p1.m_B << endl; cout << "p1的m_A: " << p1.m_C << endl; Person p2; cout << "p2的m_A: " << p1.m_A << endl; cout << "p2的m_B: " << p1.m_B << endl; cout << "p2的m_A: " << p1.m_C << endl; } int main() { test01(); system("pause"); return 0; }
4.3.4.2 类对象作为成员
在类中定义的数据成员一般都是基本的数据类型。但是类中的成员也可以是对象,叫做对象成员。
C++中对对象的初始化是非常重要的操作,当创建一个对象的时候,c++编译器必须确保调用了所有子对象的构造函数。如果所有的子对象有默认构造函数,编译器可以自动调用他们。但是如果子对象没有默认的构造函数,或者想指定调用某个构造函数怎么办?
那么是否可以在类的构造函数直接调用子类的属性完成初始化呢?但是如果子类的成员属性是私有的,我们是没有办法访问并完成初始化的。
解决办法非常简单:对于子类调用构造函数,c++为此提供了专门的语法,即构造函数初始化列表。
当调用构造函数时,首先按各对象成员在类定义中的顺序(和参数列表的顺序无关)依次调用它们的构造函数,对这些对象初始化,最后再调用本身的函数体。也就是说,先调用对象成员的构造函数,再调用本身的构造函数。
析构函数和构造函数调用顺序相反,先构造,后析构。
//汽车类 class Car{ public: Car(){ cout << "Car 默认构造函数!" << endl; mName = "大众汽车"; } Car(string name){ cout << "Car 带参数构造函数!" << endl; mName = name; } ~Car(){ cout << "Car 析构函数!" << endl; } public: string mName; }; //拖拉机 class Tractor{ public: Tractor(){ cout << "Tractor 默认构造函数!" << endl; mName = "爬土坡专用拖拉机"; } Tractor(string name){ cout << "Tractor 带参数构造函数!" << endl; mName = name; } ~Tractor(){ cout << "Tractor 析构函数!" << endl; } public: string mName; }; //人类 class Person{ public: #if 1 //类mCar不存在合适的构造函数 Person(string name){ mName = name; } #else //初始化列表可以指定调用构造函数 Person(string carName, string tracName, string name) : mTractor(tracName), mCar(carName), mName(name){ cout << "Person 构造函数!" << endl; } #endif void GoWorkByCar(){ cout << mName << "开着" << mCar.mName << "去上班!" << endl; } void GoWorkByTractor(){ cout << mName << "开着" << mTractor.mName << "去上班!" << endl; } ~Person(){ cout << "Person 析构函数!" << endl; } private: string mName; Car mCar; //编译只能调用无参的构造 Tractor mTractor; }; void test(){ //Person person("宝马", "东风拖拉机", "赵四"); Person person("刘能"); person.GoWorkByCar(); person.GoWorkByTractor(); }
代码示例1:
#define _CRT_SECURE_NO_WARNINGS #include<iostream> #include<string> using namespace std; class Phone { public: Phone() { cout << "手机的默认构造函数调用" << endl; } Phone(string name)//有参构造 { m_PhoneName = name; } ~Phone() { cout << "手机的析构函数调用" << endl; } string m_PhoneName; }; class Game { public: Game() { cout << "Game的默认构造函数调用" << endl; } Game(string name)//有参构造 { m_GameName = name; } ~Game() { cout << "Game的析构函数调用" << endl; } string m_GameName; }; class Person { public: Person() { cout << "Person的默认构造函数调用" << endl; } Person(string name) { m_Name = name; } ~Person() { cout << "Person的析构函数调用" << endl; } string m_Name;//姓名 string就不用担心深拷贝、浅拷贝的问题了,string内部会维护char *、以及释放 Phone m_Phone;//手机 Game m_Game; }; void test01() { Person p; //p.m_Phone = "诺基亚";//error /*p.m_Phone.m_PhoneName = "诺基亚"; p.m_Game.m_GameName = "斗地主";*/ } int main() { test01(); system("pause"); return 0; }
类对象作为类成员的时候
构造顺序:将类中成员的对象先进行构造,然后再构造自己
析构顺序:与构造顺序相反
#define _CRT_SECURE_NO_WARNINGS #include<iostream> #include<string> using namespace std; class Phone { public: Phone() { cout << "Phone的默认构造函数调用" << endl; } Phone(string name)//有参构造 { cout << "Phone的有参构造调用" << endl; m_PhoneName = name; } ~Phone() { cout << "Phone的析构函数调用" << endl; } string m_PhoneName; }; class Game { public: Game() { cout << "Game的默认构造函数调用" << endl; } Game(string name)//有参构造 { cout << "Game的有参构造调用" << endl; m_GameName = name; } ~Game() { cout << "Game的析构函数调用" << endl; } string m_GameName; }; class Person { public: Person() { cout << "Person的默认构造函数调用" << endl; } Person(string name,string phoneName,string gameName) :m_Name(name), m_Phone(phoneName), m_Game(gameName) { cout << "Person的有参构造调用" << endl; } void playGame() { cout << m_Name << "拿着 " << m_Phone.m_PhoneName << "牌子手机,玩着" << m_Game.m_GameName << "游戏" << endl; } ~Person() { cout << "Person的析构函数调用" << endl; } string m_Name;//姓名 string就不用担心深拷贝、浅拷贝的问题了,string内部会维护char *、以及释放 Phone m_Phone;//手机 Game m_Game; }; void test01() { //默认构造 //Person p; //p.m_Phone = "诺基亚";//error /*p.m_Phone.m_PhoneName = "诺基亚"; p.m_Game.m_GameName = "斗地主";*/ //初始化列表构造 Person p("张三", "诺基亚", "LOL"); p.playGame(); } int main() { test01(); system("pause"); return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)