c++类和对象
类和对象初阶类的定义在C语言中,只有结构体的概念,没有类,而C是兼容C语言的,所以在C中定义类有两种方式struct classname { //成员变量 //成员函数 }; class classname { //成员变量 //成员函数 };这两种定义方式肯定是有区别的,后边再说(public和private区别)定义类一定要注意,{}结尾有分号,其实基本和C语言定义结构体是一样的,只不过里面可以定义函数我们这样就可以简单写一个类了#include iostream using namespace std; class Date { public: void Init(int year, int month, int day) { _year year; _month month; _day day; } private: // 为了区分成员变量⼀般习惯上成员变量 // 会加⼀个特殊标识如_ 或者 m开头 int _year; int _month; int _day; }; int main() { Date d; d.Init(2026, 7, 11); return 0; }类的访问限定符public公有protected受保护的private私有的1. public修饰的成员在类外可以直接被访问protected和private修饰的成员在类外不能直接被访问protected和private是一样的以后继承章节才能体现出他们的区别。2. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止如果后面没有访问限定符作用域就到 }即类结束。3. class定义成员没有被访问限定符修饰时默认为privatestruct默认为public。4. 一般成员变量都会被限制为private/protected需要给别人使用的成员函数会放为public。类的作用域类定义了一个新的作用域类的所有成员都在类的作用域中在类体外定义成员时需要使用 :: 作用域操作符指明成员属于哪个类域。比如要声明和定义分离:class Date { public: void print(); private: int _year; int _month; int _day; }; void Date::print() {}类的实例化类的实例化就是创建对象一个类可以实例化出多个对象,class Date { public: void print(); private: int _year; int _month; int _day; }; int main() { Date d; return 0; }类本身是没有空间的, 实例化出对象才会分配空间, 类就好像建造图纸一样, 而对象才是房子我们可以在栈上定义局部对象,也可以在堆上申请,切记要释放哦类对象大小的计算在C语言中可以使用sizeof来计算对象所占的空间,单位是字节,比如 sizeof(int)的返回值就是4 C是兼容C语言的,那么如何计算一个对象的大小呢规则和C语言计算结构体是一样的,那么怎么计算呢,我们来看看,涉及到的问题就是结构体内存对齐内存对齐规则1. 第一个成员在与结构体偏移量为0的地址处。2. 其他成员变量要对齐到某个数字对齐数的整数倍的地址处。3. 对齐数 编译器默认的一个对齐数 与 该成员大小的较小值。4. VS中默认的对齐数为8。5. 结构体总大小为最大对齐数所有变量类型最大者与默认对齐参数取最小的整数倍。6. 如果嵌套了结构体的情况嵌套的结构体对齐到自己的最大对齐数的整数倍处结构体的整体大小就是所有最大对齐数含嵌套结构体的对齐数的整数倍。// 计算⼀下A/B/C实例化的对象是多⼤ class A { public: void Print() { cout _ch endl; } private: char _ch; int _i; }; class B { public: void Print() { //... } }; class C {};class A成员char (1)、int (4) char 占 0 偏移int 需对齐到 4 偏移中间补 3 字节总 8是最大对齐数 4 的倍数。 sizeof (A) 8class B只有成员函数无成员变量空类。 sizeof (B) 1class C完全空类。 sizeof (C) 1上面的程序运行后我们看到没有成员变量的B和C类对象的大小是1为什么没有成员变量还要给1个字节呢因为如果一个字节都不给怎么表示对象存在过呢所以这里给1字节纯粹是为了占位标识对象存在。this指针#includeiostream using namespace std; class Date { public: // void Init(Date* const this, int year, int month, int day) void Init(int year, int month, int day) { // 编译报错error C2106: “”: 左操作数必须为左值 // this nullptr; // this-_year year; _year year; this-_month month; this-_day day; } void Print() { cout _year / _month / _day endl; } private: // 这⾥只是声明没有开空间 int _year; int _month; int _day; }; int main() { // Date类实例化出对象d1和d2 Date d1; Date d2; // d1.Init(d1, 2024, 3, 31); d1.Init(2024, 3, 31); d1.Print(); d2.Init(2024, 7, 5); d2.Print(); return 0; }1. Date类中有 Init 与 Print 两个成员函数函数体中没有关于不同对象的区分那当d1调用Init和Print函数时该函数是如何知道应该访问的是d1对象还是d2对象呢C通过隐含的this指针解决该问题。2. 编译器编译后类的成员函数默认会在形参第一个位置增加一个当前类类型的指针即this指针。例如Date类Init真实原型void Init(Date* const this, int year, int month, int day)。3. 类成员函数内访问成员变量本质都是通过this指针访问如赋值this-_year year;。4. C规定不能在实参、形参位置显式书写this指针由编译器自动处理但可以在函数体内显式使用this指针。this指针相关题目 //下面程序编译运行结果是 //A、编译报错 B、运行崩溃 C、正常运行 #includeiostream using namespace std; class A { public: void Print() { cout A::Print() endl; } private: int _a; }; int main() { A* p nullptr; p-Print(); return 0; } 可以看到结果是正常运行 那为什么呢??? p是空指针!!! 这里把p当做this传入了Print()函数中 并没有对nullptr解引用只是传入的this是空指针改变一下//下面程序编译运行结果是 //A、编译报错 B、运行崩溃 C、正常运行 #includeiostream using namespace std; class A { public: void Print() { cout A::Print() endl; cout _a endl; } private: int _a; }; int main() { A* p nullptr; p-Print(); return 0; }这个比较简单,肯定会崩溃,因为_a 相当于是this-_a,会对空指针解引用类和对象进阶六大默认成员函数构造函数构造函数主要完成资源的初始化析构函数析构函数主要完成资源的清理和回收拷贝构造拷贝构造主要完成用同类对象初始化自身的情况赋值运算符重载赋值重载完成的是把一个对象赋值给另一个对象普通对象取地址就是默认取到对象地址如果想要别人获取到指定内容可以重载const 对象取地址与普通对象取地址基本相同只不过是 const 对象构造函数定义1. 构造函数是特殊的成员函数需要注意的是构造函数虽然名称叫构造但是构造函数的主要任务并不是开空间创建对象(我们常使用的局部对象是栈帧创建时空间就开好了)而是对象实例化时初始化对象。2. 构造函数的本质是要替代我们以前Date类中写的Init函数的功能构造函数自动调用的特点就完美的替代的了Init。构造函数的特点1. 函数名与类名相同。2. 无返回值。 (返回值啥都不需要给也不需要写void不要纠结C规定如此)3. 对象实例化时系统会自动调用对应的构造函数。4. 构造函数可以重载。5. 如果类中没有显式定义构造函数则C编译器会自动生成一个无参的默认构造函数一旦用户显式定义编译器将不再生成。6. 无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数都叫做默认构造函数。但是这三个函数有且只有一个存在不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载但是调用时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认生成那个叫默认构造实际上无参构造函数、全缺省构造函数也是默认构造总结一下就是不传实参就可以调用的构造就叫默认构造。7. 我们不写编译器默认生成的构造对内置类型成员变量的初始化没有要求也就是说是是否初始化是不确定的看编译器。对于自定义类型成员变量要求调用这个成员变量的默认构造函数初始化。如果这个成员变量没有默认构造函数那么就会报错我们要初始化这个成员变量需要用初始化列表才能解决初始化列表我们下个章节再细细讲解。//说明C把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的原生数据类型如int/char/double/指针等自定义类型就是我们使用class/struct等关键字自己定义的类型。#include iostream using namespace std; class Date { public: //1.无参构造函数 Date() { _year 1; _month 1; _day 1; } //2.带参构造函数 Date(int year, int month, int day) { _year year; _month month; _day day; } //3.全缺省构造函数 /*Date(int year 1, int month 1, int day 1) { _year year; _month month; _day day; }*/ void Print() { cout _year / _month / _day endl; } private: int _year; int _month; int _day; }; int main() { // 如果留下三个构造中的第⼆个带参构造第⼀个和第三个注释掉 // 编译报错error C2512: “Date”: 没有合适的默认构造函数可⽤ Date d1; // 调⽤默认构造函数 Date d2(2025, 1, 1); // 调⽤带参的构造函数 // 注意如果通过⽆参构造函数创建对象时对象后⾯不⽤跟括号否则编译器⽆法 // 区分这⾥是函数声明还是实例化对象 // warning C4930: “Date d3(void)”: 未调⽤原型函数(是否是有意⽤变量定义的?) Date d3(); d1.Print(); d2.Print(); return 0; }析构函数定义析构函数的功能和构造函数基本是相反的,构造是初始化,析构是清理资源,所以它们的特性也很相似与构造函数功能相反析构函数不是完成对对象本身的销毁局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数完成对象中资源的清理工作特点1. 析构函数名是在类名前加上字符 ~2. 无参数无返回值。 (这里跟构造类似也不需要加void)3. 一个类只能有一个析构函数。若未显式定义系统会自动生成默认的析构函数。4. 对象生命周期结束时系统会自动调用析构函数。5. 跟构造函数类似我们不写编译器自动生成的析构函数对内置类型成员不做处理自定义类型成员会调用它的析构函数。6. 还需要注意的是我们显式写析构函数对于自定义类型成员也会调用它的析构也就是说自定义类型成员无论什么情况都会自动调用析构函数。7.如果类中没有申请资源时析构函数可以不写直接使用编译器生成的默认析构函数如Date如果默认生成的析构就可以用也就不需要显示写析构如MyQueue但是有资源申请时一定要自己写析构否则会造成资源泄漏如Stack。8. 一个局部域的多个对象C规定后定义的先析构。#includeiostream using namespace std; typedef int STDataType; class Stack { public: Stack(int n 4) { _a (STDataType*)malloc(sizeof(STDataType) * n); if (nullptr _a) { perror(malloc申请空间失败); return; } _capacity n; _top 0; } ~Stack() { cout ~Stack() endl; free(_a); _a nullptr; _top _capacity 0; } private: STDataType* _a; size_t _capacity; size_t _top; }; // 两个Stack实现队列 class MyQueue { public: //编译器默认生成MyQueue的析构函数调用了Stack的析构释放的Stack内部的资源 // 显示写析构也会自动调用Stack的析构 /*~MyQueue() {}*/ private: Stack pushst; Stack popst; }; int main() { Stack st; MyQueue mq; return 0; }我们什么时候需要写析构函数析构函数是完成资源清理的如果此类中有从堆上申请的资源使用new/malloc这些开辟出来的空间就需要写析构函数如果没有申请的资源编译器自动生成的就可以对自定义类型去调用它的析构函数对内置类型不做处理拷贝构造函数如果一个构造函数的第一个参数是自身类类型的引用且任何额外的参数都有默认值则此构造函数也叫做拷贝构造函数也就是说拷贝构造是一个特殊的构造函数。拷贝构造的特点拷贝构造函数是构造函数的一个重载。拷贝构造函数的第一个参数必须是类类型对象的引用使用传值方式编译器直接报错因为语法逻辑上会引发无穷递归调用。拷贝构造函数也可以多个参数但是第一个参数必须是类类型对象的引用后面的参数必须有缺省值。C 规定自定义类型对象进行拷贝行为必须调用拷贝构造所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。若未显式定义拷贝构造编译器会自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝 / 浅拷贝 (一个字节一个字节的拷贝)对自定义类型成员变量会调用它的拷贝构造。像 Date 这样的类成员变量全是内置类型且没有指向什么资源编译器自动生成的拷贝构造就可以完成需要的拷贝所以不需要我们显式实现拷贝构造。像 Stack 这样的类虽然也都是内置类型但是_a 指向了资源编译器自动生成的拷贝构造完成的值拷贝 / 浅拷贝不符合我们的需求所以需要我们自己实现深拷贝 (对指向的资源也进行拷贝)。像 MyQueue 这样的类型内部主要是自定义类型 Stack 成员编译器自动生成的拷贝构造会调用 Stack 的拷贝构造也不需要我们显式实现 MyQueue 的拷贝构造。这里还有一个小技巧如果一个类显式实现了析构并释放资源那么他就需要显式写拷贝构造否则就不需要。传值返回会产生一个临时对象调用拷贝构造传值引用返回返回的是返回对象的别名 (引用)没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象函数结束就销毁了那么使用引用返回是有问题的这时的引用相当于一个野引用类似一个野指针一样。传引用返回可以减少拷贝但是一定要确保返回对象在当前函数结束后还在才能用引用返回。拷贝构造写法拷贝构造函数只有单个形参该形参是对本类类型对象的引用(一般常用const修饰)在用已存在的类类型对象创建新对象时由编译器自动调用.//拷贝构造 Date(const Date d) { _year d._year; _month d._month; _day d._day; }拷贝构造是构造函数的重载,拷贝构造函数的参数是当前类型的引用我们不写编译器默认生成的拷贝构造会做什么事??默认的拷贝构造函数对象按内存存储按字节序完成拷贝这种拷贝叫做浅拷贝或者值拷贝。编译器默认生成的拷贝构造对自定义类型是调用它的拷贝构造 ,对于内置类型,进行浅拷贝赋值运算符重载运算符重载C为了增强代码的可读性引入了运算符重载运算符重载是具有特殊函数名的函数也具有其返回值类型函数名字以及参数列表其返回值类型与参数列表与普通的函数类似类对象使用运算符可重载赋予新语义无对应重载则编译报错。重载函数命名为operator运算符含返回值、参数、函数体。参数数量匹配运算符操作数一元 1 参、二元 2 参成员函数会占用 this参数少 1 个。重载不改变运算符原有优先级、结合性。不能自创新运算符.*、::、sizeof、?:、.禁止重载。重载必须含至少一个类类型参数不可修改内置类型运算逻辑。仅重载有实际意义的运算符。自增区分前置operator()后置多加 int 形参operator(int)。/需全局重载保证流对象在左操作数符合使用习惯Date类的运算符重载就是这样运算符重载bool operator(const Date d) { return _year d._year _month d._month _day d._day; }取地址运算符重载const成员函数精简总结1. const成员函数const写在函数参数列表后方修饰成员函数。2. const修饰隐式this指针函数内不允许修改类任何成员。3. 例Date类Print加const后this从Date* const this变为const Date* const this。// void Print(const Date* const this) const void Print() const { cout _year - _month - _day endl; }取地址运算符重载1. 分普通、const 两种重载版本。2. 编译器默认生成的版本大多够用无需手动实现。3. 特殊场景禁止他人获取对象真实地址可自定义随意返回地址。class Date { public : Date* operator() { return this; // return nullptr; } const Date* operator() const { return this; //return nullptr; } private : int _year ; // 年 int _month ; // ⽉ int _day ; // ⽇ };类和对象后阶初始化列表class Date { public: Date(int x, int year 1, int month 1, int day 1) :_year(year) , _month(month) , _day(day) , _t(12) , _ref(x) , _n(1) { // error C2512: “Time”: 没有合适的默认构造函数可用 // error C2530 : “Date::_ref” : 必须初始化引用 // error C2789 : “Date::_n” : 必须初始化常量限定类型的对象 } void Print() const { cout _year - _month - _day endl; } private: int _year; int _month; int _day; Time _t; // 没有默认构造 int _ref; // 引用 const int _n; // const };private: // 注意这⾥不是初始化这⾥给的是缺省值这个缺省值是给初始化列表的 // 如果初始化列表没有显⽰初始化默认就会⽤这个缺省值初始化 int _year 1; int _month 1; int _day; Time _t 1; const int _n 1; int* _ptr (int*)malloc(12);初始化列表的特性一、初始化列表语法限制每个成员变量在初始化列表中只能出现一次成员变量的初始化操作仅能执行一次重复书写无意义。二、必须在初始化列表初始化的三类成员(上面代码有显示)有三种成员无法在构造函数函数体内赋值定义时就必须完成初始化只能写在初始化列表中const 修饰的成员变量 const 变量一经创建就不可修改无法在函数体内赋值只能初始化时给定值。引用类型成员变量 引用创建时必须绑定一个有效变量实体不存在空引用不能后续赋值绑定只能在初始化列表绑定对象。无默认构造函数的自定义类型成员 该自定义类没有无参构造编译器无法自动完成初始化必须手动在初始化列表调用带参构造。三、推荐优先使用初始化列表初始化底层执行机制无论构造函数是否手写初始化列表程序都会完整执行初始化列表流程所有成员都会走初始化阶段仅内置类型若无显式初始化编译器不会做处理。性能区别初始化列表直接调用对应构造函数仅执行一次构造无额外开销函数体内赋值自定义成员会先调用默认构造再执行赋值运算符重载多一次对象拷贝效率更低。总结所有成员尽量统一在初始化列表完成初始化兼顾语法合规与代码性能。类型转换C 支持内置类型隐式转换成类对象前提是存在以该内置类型为参数的构造函数。在构造函数前添加explicit关键字会禁止该构造函数触发隐式类型转换。类与类之间也能发生隐式转换需要对应构造函数提供支持。class A { public: // 构造函数explicit就不再⽀持隐式类型转换 // explicit A(int a1) A(int a1) :_a1(a1) { } //explicit A(int a1, int a2) A(int a1, int a2) :_a1(a1) , _a2(a2) { } void Print() { cout _a1 _a2 endl; } int Get() const { return _a1 _a2; } private: int _a1 1; int _a2 2; }; class B { public: B(const A a) :_b(a.Get()) { } private: int _b 0; }; int main() { // 1构造⼀个A的临时对象再⽤这个临时对象拷⻉构造aa3 // 编译器遇到连续构造拷⻉构造-优化为直接构造 A aa1 1; aa1.Print(); const A aa2 1; // C11之后才⽀持多参数转化 A aa3 { 2,2 }; // aa3隐式类型转换为b对象 // 原理跟上⾯类似 B b aa3; const B rb aa3; return 0; }static成员类中的成员也可以是静态的,我们来瞅瞅声明为static的类成员称为类的静态成员用static修饰的成员变量称之为静态成员变量用static修饰的成员函数称之为静态成员函数。静态成员变量一定要在类外进行初始化静态成员的特性1.静态成员为所有类对象所共享不属于某个具体的对象存放在静态区2.静态成员变量必须在类外定义定义时不添加static关键字类中只是声明3.类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问4.静态成员函数没有隐藏的this指针不能访问任何非静态成员5.静态成员也是类的成员受public、protected、private 访问限定符的限制// 实现⼀个类计算程序中创建出了多少个类对象 #includeiostream using namespace std; class A { public: A() { _scount; } A(const A t) { _scount; } ~A() { --_scount; } static int GetACount() { return _scount; } private: // 类⾥⾯声明 static int _scount; }; // 类外⾯初始化 int A::_scount 0; int main() { cout A::GetACount() endl; A a1, a2; A a3(a1); cout A::GetACount() endl; cout a1.GetACount() endl; // 编译报错error C2248: “A::_scount”: ⽆法访问 private 成员(在“A”类中声明) //cout A::_scount endl; return 0; }友元友元提供了一种突破类访问限定符封装的方式友元分为友元函数和友元类在函数声明或者类声明的前面加friend并且把友元声明放到一个类的里面。友元的特性• 外部友元函数可访问类的私有和保护成员友元函数仅仅是一种声明他不是类的成员函数。• 友元函数可以在类定义的任何地方声明不受类访问限定符限制。• 一个函数可以是多个类的友元函数。• 友元类中的成员函数都可以是另一个类的友元函数都可以访问另一个类中的私有和保护成员。• 友元类的关系是单向的不具有交换性比如A类是B类的友元但是B类不是A类的友元。• 友元类关系不能传递如果A是B的友元 B是C的友元但是A不是C的友元。• 有时提供了便利。但是友元会增加耦合度破坏了封装所以友元不宜多用。#includeiostream using namespace std; // 前置声明都则A的友元函数声明编译器不认识B class B; class A { // 友元声明 friend void func(const A aa, const B bb); private: int _a1 1; int _a2 2; }; class B { // 友元声明 friend void func(const A aa, const B bb); private: int _b1 3; int _b2 4; }; void func(const A aa, const B bb) { cout aa._a1 endl; cout bb._b1 endl; } int main() { A aa; B bb; func(aa, bb); return 0; }内部类如果一个类定义在另一个类的内部这个内部类就叫做内部类。内部类是一个独立的类跟定义在全局相比他只是受外部类类域限制和访问限定符限制所以外部类定义的对象中不包含内部类。• 内部类默认是外部类的友元类。• 内部类本质也是一种封装当A类跟B类紧密关联A类实现出来主要就是给B类使用那么可以考虑把A类设计为B的内部类如果放到private/protected位置那么A类就是B类的专属内部类其他地方都用不了。#includeiostream using namespace std; class A { private: static int _k; int _h 1; public: class B // B默认就是A的友元 { public: void foo(const A a) { cout _k endl; //OK cout a._h endl; //OK } int _b1; }; }; int A::_k 1; int main() { cout sizeof(A) endl; A::B b; A aa; b.foo(aa); return 0; }匿名对象・用 类型 (实参) 定义出来的对象叫做匿名对象相比之前我们定义的 类型 对象名 (实参) 定义出来的叫有名对象・匿名对象生命周期只在当前一行一般临时定义一个对象当前用一下即可就可以定义匿名对象。int main() { A aa1; // 不能这么定义对象因为编译器⽆法识别下⾯是⼀个函数声明还是对象定义 //A aa1(); // 但是我们可以这么定义匿名对象匿名对象的特点不⽤取名字 // 但是他的⽣命周期只有这⼀⾏我们可以看到下⼀⾏他就会⾃动调⽤析构函数 A(); A(1); A aa2(2); // 匿名对象在这样场景下就很好⽤当然还有⼀些其他使⽤场景这个我们以后遇到了再说 Solution().Sum_Solution(10); return 0; }拷贝对象时优化现代编译器会为了尽可能提高程序的效率在不影响正确性的情况下会尽可能减少一些传参和传返回值的过程中可以省略的拷贝。• 如何优化C标准并没有严格规定各个编译器会根据情况自行处理。当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化有些更新更激进的编译器还会进行跨行跨表达式的合并优化。• linux下可以将下面代码拷贝到test.cpp文件编译时用 g test.cpp -fno-elide-constructors 的方式关闭构造相关的优化。#includeiostream using namespace std; class A { public: A(int a 0) :_a1(a) { cout A(int a) endl; } A(const A aa) :_a1(aa._a1) { cout A(const A aa) endl; } A operator(const A aa) { cout A operator(const A aa) endl; if (this ! aa) { _a1 aa._a1; } return *this; } ~A() { cout ~A() endl; } private: int _a1 1; }; void f1(A aa) {} A f2() { A aa; return aa; } int main() { // 传值传参 // 构造拷贝构造 A aa1; f1(aa1); cout endl; // 隐式类型连续构造拷贝构造-优化为直接构造 f1(1); // 一个表达式中连续构造拷贝构造-优化为一个构造 f1(A(2)); cout endl; cout *********************************************** endl; // 传值返回 // 不优化的情况下传值返回编译器会生成一个拷贝返回对象的临时对象作为函数调用表达式的返回值 // 无优化 vs2019 debug // 一些编译器会优化得更厉害将构造的局部对象和拷贝构造的临时对象优化为直接构造vs2022 debug f2(); cout endl; // 返回时一个表达式中连续拷贝构造拷贝构造-优化一个拷贝构造 vs2019 debug // 一些编译器会优化得更厉害进行跨行合并优化将构造的局部对象aa和拷贝的临时对象和接收返回值对象aa2优化为一个直接构造。vs2022 debug A aa2 f2(); cout endl; // 一个表达式中开始构造中间拷贝构造赋值重载-无法优化vs2019 debug // 一些编译器会优化得更厉害进行跨行合并优化将构造的局部对象aa和拷贝临时对象合并为一个直接构造vs2022 debug aa1 f2(); cout endl; return 0; }好了,类和对象就到这里了,类和对象是C学习的基础,所以一定要多加练习,感谢收看
