ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C++类和对象【下】

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.不能通过连接语法中没有的符号来创建新的操作符:比如operator@

2.(.*) (::) (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 this

6.初始化列表

之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有⼀种方式,就是初始化列表

初始化列表的使用方式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成员列表,每个"成员变量"后面跟⼀个放在括号中的初始值或表达式

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.C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的

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类的专属内部类,其 他地方都用不了

就比如下面这一道题:

求1+2+3+...+n_牛客题霸_牛客网https://www.nowcoder.com/practice/7a0da8fc483247ff8800059e12d7caf1?tpId=13&tqId=11200&tPage=3&rp=3&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking

class 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; }

匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象

好了,以上就是类和对象的全部内容了,感谢阅读,希望能够帮到你,也欢迎大家指错及补充

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进