C++类和对象【下】 目录4.3拷贝构造函数4.4赋值运算符重载运算符重载什么是运算符重载如何定义运算符重载定义运算符重载时的注意事项5.const成员函数6.初始化列表7.类型转换8.static成员8.1static修饰成员变量8.2static修饰成员函数9.友元10.内部类11.匿名对象续上4.3拷贝构造函数如果⼀个构造函数的第一个参数是自身类类型的引用且任何额外的参数都有默认值则此构造函数也叫做拷贝构造函数也就是说拷贝构造是一个特殊的构造函数。拷贝构造的特点1.拷贝构造是构造函数的重载2.拷贝构造的第一个参数必须是自生类类型的引用因为在传参过程中其实实际上也是一个拷贝而类类型就会调用拷贝构造去初始化形参这样就会陷入无限递归3.拷贝构造函数也可以多个参数但是第一个参数必须是类类型对象的引用后面的参数必须有缺省值class Date { Date(const Date d, int day 0 /* ...其他参数(有默认值) */)// 拷贝构造 { // ... } };4.若未显式定义拷贝构造则编译器会默认生成一个拷贝构造且该拷贝是对内置类型的变量是浅拷贝对自定义类型变量则会去调用它的拷贝构造浅拷贝一个字节一个字节的拷贝class Date { public: Date(int year 0, int month 0, int day 0) :_year(year), _month(month), _day(day) {} Date(const Date d) { _year d._year; _month d._month; _day d._day; } private: int _year; int _month; int _day; }; class news { public: news(const Date val, const char* _str) { d val; weather new char[strlen(_str) 1]; strcpy(weather, _str); } private: Date d; char* weather; }; int main() { Date d(2026, 4, 28); news n1(d, 晴); news n2(n1);// 未实现news拷贝构造,weather直接浅拷贝,Date类型调用Date类类型的拷贝构造 return 0; }可以看到n1和n2的weather指向同一处地址这样导致改变其中一处的weather会导致另一处的更改这就是浅拷贝的坏处所以这时候就要自己来实现拷贝构造(实现深拷贝)也就是说若类成员变量指向了堆上的资源就要实现深拷贝同时因为申请了堆上的空间就要实现析构函数所以需要我们自己实现析构函数的类都需要写拷贝构造否则不需要news(const news n)// 拷贝构造 { d n.d; weather new char[strlen(n.weather) 1]; strcpy(weather, n.weather); } ~news()// 析构函数 { delete[] weather; }4.4赋值运算符重载运算符重载什么是运算符重载当运算符被用于类类型的对象时C允许我们通过运算符重载的形式指定新的含义。C规定类类型对象使用运算符时必须转换成调用对应运算符重载若没有对应的运算符重载则会编译报错就比如我们在日期类要让日期 d 加上1我们想要的是d中的“day”加1但是编译器并不知道所以就有了运算符重载让我们自己去实现这些定义如何定义运算符重载1.运算符重载是具有特殊名字的函数他的名字是由operator和后面要定义的运算符共同构成。和其他函数⼀样它也具有其返回类型和参数列表以及函数体2.如果一个重载运算符函数是成员函数则它的第⼀个运算对象默认传给隐式的this指针因此运算符重载作为成员函数时参数比运算对象少一个3.重载运算符函数的参数个数和该运算符作用的运算对象数量⼀样多。一元运算符有⼀个参数二元运算符有两个参数并且二元运算符的左操作数为它的第一个参数右操作数为第二个参数4.运算符重载以后其优先级和结合性与对应的内置类型运算符保持⼀致以赋值运算符为例定义运算符重载时的注意事项1.不能通过连接语法中没有的符号来创建新的操作符比如operator2.(.*) (::) (sizeof) ( ? : ) ( . )该五个运算符不能实现函数重载用小括号做了区分3.重载操作符至少有⼀个类类型参数且不能通过运算符重载改变内置类型对象的含义比如int operator(int a, int b) { return a - b; }4.⼀个类需要重载哪些运算符是看哪些运算符重载后有意义比如Date类重载operator-就有意 义日期减日期 天数但是重载operator就没有意义日期加日期 5.重载运算符时有前置和后置运算符重载函数名都是operator就无法很好的区分。 所以C规定后置重载时增加⼀个int形参跟前置构成函数重载方便区分。Date operator()// 前置 { // ... } Date operator(int)// 后置 { // ... }6.重载二元运算符 或 时推荐重载为全局函数搭配友元因为重载为成员函数this指针抢占了第一个参数的位置调用时就不符合习惯可读性也差了(这里仅做演示这里用了友元的知识代码细节不必考虑后面会细讲)重载为成员函数重载为全局函数至此有了以上理论基础就能够实现赋值运算符重载了Date operator(const Date d) { _year d._year; _month d._month; _day d._day; return *this; }当然默认生成的赋值运算符重载依旧是浅拷贝想日期类就不用我们自己生成5.const成员函数我们之前已经说过每一个成员函数都会有一个默认不显示的隐藏参数——this指针我们也知道如果不想实参在函数中被修改我们就可以用const来修饰那如果我们不想让成员函数去修改this指针指向的对象的数据我们该如何让const去修饰这个隐藏的参数呢答案是将const放到成员函数参数列表的后面让const修饰成员函数例如int operator-(const Date d) const { // ... }被const修饰的成员函数称之为const成员函数表明在该成员函数中不能对类的任何成员进行修改this指针Date* const this ----- const Date* const this6.初始化列表之前我们实现构造函数时初始化成员变量主要使用函数体内赋值构造函数初始化还有⼀种方式就是初始化列表初始化列表的使用方式是以⼀个冒号开始接着是⼀个以逗号分隔的数据成员列表每个成员变量后面跟⼀个放在括号中的初始值或表达式class Date { public: //Date(int year, int month, int day)// 构造函数 //{ // _year year; // _month month; // _day day; //} Date(int year, int month, int day)// 初始化列表 :_year(year), _month(month), _day(day) {} private: int _year; int _month; int _day; }在使用初始化列表时要注意以下事项1.每个成员变量在初始化列表中只能出现一次语法理解上初始化列表可以认为是每个成员变量定义初始化的地方2.引用成员变量const成员变量没有默认构造的类类型变量必须放在初始化列表位置进行初始化否则会编译报错class Date// 无默认构造的类 { public: Date(int year, int month, int day) :_year(year), _month(month), _day(day) {} Date(const Date d) { _year d._year; _month d._month; _day d._day; } private: int _year; int _month; int _day; } class news { public: news(const Date val, char val_char, int val_int) :d(val) , a(val_char) , b(val_int) {} private: Date d; const char a; int b; };3.C11支持在成员变量声明的位置给缺省值这个缺省值主要是给没有显示在初始化列表初始化的成员使用的4.初始化列表中按照成员变量在类中声明顺序进行初始化跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持⼀致我们在初始化类成员变量时尽量使用初始化列表初始化因为你不在初始化列表初始化的成员也都会走初始化列表如果这个变量在声明时给了缺省值则会用缺省值进行初始化如果没有缺省值对于内置类型的初始化情况就取决于编译器了而对于自定义类型则会调用他的默认构造如果没有则会报错总结7.类型转换C中支持内置类型隐式类型转化为类类型对象前提是有对应的内置类型参数的构造函数类类型的对象之间也可以隐式转换需要相应的构造函数支持class A { public: A(int val) :_a(val) {} int GetVal() const { return _a; } private: int _a; }; class B { public: B(const A a) :b(a.GetVal()) {} private: int b; }; int main() { A a(1); B b(a); return 0; }如果我们不想再支持隐式类型转换就在构造函数前加上explicit就行8.static成员用static修饰的变量或成员叫做静态成员变量或静态成员函数非静态的成员函数可以访问任意的静态成员变量和静态成员函数突破类域就可以访问静态成员可以通过“ 类名::静态成员 ”或者“ 对象.静态成员 ”来访问静态成员变量和静态成员函数静态成员也是类的成员受访问限定符的限制8.1static修饰成员变量用static修饰的成员变量叫做静态成员变量静态成员变量是要在类外进行初始化静态成员变量为所有类对象所共享不属于某个具体的对象不存在对象中存放在静态区namespace zhangsan { class string { public: // ... static size_t npos;// 类中声明 void test() { cout npos endl; } private: char* _str; size_t _size; size_t _capacity; }; size_t string::npos -1;// 类外进行初始化 }注不能再声明处给缺省值因为缺省值是给走初始化列表的成员变量进行初始化的而静态成员变量不走初始化列表8.2static修饰成员函数用static修饰成员函数叫做静态成员函数静态成员函数中没有this指针静态成员函数可以访问其他静态成员变量但不能访问非静态的因为没有this指针9.友元友元提供了一种突破类访问限定符封装的方式。友元分为友元函数和友元类在函数声明或者类 声明的前面加“ friend ”并且把友元声明放到⼀个类的里面。外部友元函数可访问类的私有和保护成员友元函数仅仅是⼀种声明他不是类的成员函数友元函数可以在类定义的任何地方声明不受类访问限定符限制⼀个函数可以是多个类的友元函数// 前置声明 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; }友元类中的成员函数都可以是另⼀个类的友元函数都可以访问另⼀个类中的私有和保护成员class A { public: // 友元声明 A() { // ... } friend class B; private: int _a1; int _a2; }; class B { public: B() { // ... } void func1(const A aa) { cout aa._a1 endl; cout _b1 endl; } void func2(const A aa) { cout aa._a2 endl; cout _b2 endl; } private: int _b1; int _b2; };我们在上面实现 或 运算符重载时由于友元函数的特性我们就也用了友元函数这样一来就符合了调用时的习惯也提高了代码的可读性友元类的关系是单向的不具有交换性比如A类是B类的友元但是B类不是A类的友元友元类关系不能传递如果A是B的友元B是C的友元但是A不是C的友元注友元虽然提供了便利但是友元会增加耦合度破坏了封装所以友元不宜多用10.内部类如果⼀个类定义在另⼀个类的内部这个内部类就叫做内部类内部类是⼀个独立的类他只是受外部类类域限制和访问限定符限制所以外部类定义的对象中不包含内部类内部类默认是外部类的友元类class A { private: static int _k; int _h 1; public: class B // B默认就是A的友元 { public: void foo(const A a) { cout _k endl; cout a._h endl; } int _b1; }; }; int A::_k 1; int main() { A a; A::B b; b.foo(a); return 0; }内部类本质也是⼀种封装当A类跟B类紧密关联A类实现出来主要就是给B类使用那么可以考 虑把A类设计为B的内部类如果放到private/protected位置那么A类就是B类的专属内部类其 他地方都用不了就比如下面这一道题求123...n_牛客题霸_牛客网https://www.nowcoder.com/practice/7a0da8fc483247ff8800059e12d7caf1?tpId13tqId11200tPage3rp3ru/ta/coding-interviewsqru/ta/coding-interviews/question-rankingclass Solution { class Sum { public: Sum() { sum i; i; } }; public: int Sum_Solution(int n) { Sum arr[n]; return sum; } static size_t sum; static size_t i; }; size_t Solution::sum 0; size_t Solution::i 1;这种解法就很清楚地体现内部类的用法11.匿名对象用类型(实参)定义出来的对象叫做匿名对象相比之前我们定义的类型对象名(实参)定义出来的叫有名对象class A { public: A(const int val) :a(val) {} void func() { cout a endl; } private: int a; }; int main() { A(1).func();// 匿名对象 A a(2);// 有名对象 a.func(); return 0; }匿名对象生命周期只在当前一行一般临时定义一个对象当前用一下即可就可以定义匿名对象好了以上就是类和对象的全部内容了感谢阅读希望能够帮到你也欢迎大家指错及补充