C++类和对象相关内容¶
约 3824 个字 972 行代码 预计阅读时间 25 分钟
explicit
关键字¶
在C++中,给类对象初始化时会调用类的构造函数,但是也可以使用赋值运算符为构造函数只有一个参数(或者只有一个参数没有缺省值)的类对象赋值,如下面代码
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
在上面的代码中,test
类对象初始化时需要调用有一个参数的构造函数(对应test t(1)
),而也可以直接使用赋值运算符,将初始化值赋值给类对象,这个过程经历了:调用构造函数使用整型1为临时对象初始化,再调用拷贝构造函数将临时对象拷贝给t1
对象,这个过程也是一种类型转换,但是实际上这个过程一般会被编译器优化为直接调用构造函数,使用整型1对类对象初始化,即优化过程:构造函数+拷贝构造函数->构造函数
而如果不想以上面的方式,只用直接调用构造函数的方式对类对象进行初始化时,可以使用explicit
关键字对构造函数进行修饰
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
当构造函数被explicit
关键字修饰后,test t1 = 1
的初始化方式失效
在C++11标准规范中,也支持对不只有一个参数的构造函数使用对类对象进行赋值初始化的方式
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
同样,如果不愿意使用直接赋值的方式为类对象进行初始化时,可以使用explicit
关键字修饰构造函数
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
但是,如果为已经实例化的对象再次赋值时,则会调用赋值运算符重载函数为对象赋值,如下面代码
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
在上面的代码中,t
对象再次赋值时会调用赋值重载,首先会调用拷贝构造函数,但是会被编译器优化为直接调用构造函数,再调用赋值重载函数
static
成员¶
static
成员的介绍¶
当需要统计一个类创建了多少个对象时,第一反应是创建一个全局变量,当每一次调用构造函数或者拷贝构造函数时,就让其进行+1操作,但是这个思路的问题是该全局变量不仅是在类中可以访问,也可以在类外访问,此时如果在类外改变了该变量的值,那么此时的计数不一定准确,如果直接放置到类内作为类的成员变量,那么每一个类对象在创建时都会为这个成员变量分配独立的空间,那么每一个类对象的计数器变量都只为1,并没有达到计数的效果
为了解决上面的问题,C++支持在类中创建静态成员变量,该成员变量不属于任何一个实例对象,而属于整个类,但是因为在类内,所以类中的成员函数也可以直接访问该静态成员变量。一样的思路,当每一次调用构造函数或者拷贝构造函数时让该变量+1即可实现统计一个类有多少个对象,并且这个方法的好处是保证了类的封装性,如果该成员变量具有private
属性,那么在类外也不可以直接修改(要修改时需要类中提供静态set
成员函数),但是此时要在类外直接访问该静态成员变量时需要提供get
函数,如下面的代码
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
上面代码的作用是统计创建类对象的个数,基本思路是通过统计调用构造函数或者拷贝构造函数的次数来统计类对象的个数,类中存在一个静态成员变量_count
,该成员变量从属于整个类而不是某一个单独的类对象,因为是静态成员变量,所以需要使用静态成员函数才可以在类域外访问(因为具有private
属性,所以不可以直接通过类名进行调用)
static
成员的使用¶
在C++中,声明为static
的类成员称为类的静态成员,用static
修饰的成员变量,称之为静态成员变量
同样,用static
修饰的成员函数,称之为静态成员函数
Note
注意:静态成员变量一定要在类外进行初始化
static
成员的特点
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区,在使用
sizeof
计算类大小时同样不会包括static
成员的大小 - 静态成员变量必须在类外定义,定义时不添加
static
关键字,类中只是声明 - 类静态成员即可用 类名
::
静态成员 或者对象.静态成员
来访问(前提是非private
属性) - 静态成员函数没有隐藏的
this
指针,不能访问任何非静态成员 - 静态成员也是类的成员,受
public
、protected
、private
访问限定符的限制
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
例如下面的题目:
求1+2+3+...+n
题目链接:求1+2+3+...+n_牛客题霸_牛客网 (nowcoder.com)
Quote
描述:
求1+2+3+...+n,要求不能使用乘除法、for
、while
、if
、else
、switch
、case
等关键字及条件判断语句(A?B:C
)。
数据范围:0<n≤200 进阶: 空间复杂度O(1) ,时间复杂度 O(n)
参考代码
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
优化为:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
友元¶
在C++中,友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类
友元函数¶
在默认构造函数章节中,对流插入运算符和流提取运算符进行重载时,如果将这两个运算符重载函数放置在类中时,那么ostream
和istream
的对象将与this
指针抢占第一个参数的位置,导致最后调用时和正常的输出输入参数相反,而解决这个问题时考虑到将这两个重载函数放置到全局,但是放置到全局时将无法看到类成员变量,此时为了同时解决前面两个问题,考虑使用友元函数,如下面代码
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
|
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend
关键字
友元函数的特点:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用
const
修饰 - 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
友元类¶
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
友元类的特点:
- 友元关系是单向的,不具有交换性
-
友元关系不能传递
Note
若C是B的友元类(即C想使用B类中的成员),B是A的友元类(即B想使用A类中的成员),不能推出C是A的友元类(即C依旧无法使用A中的成员)
-
友元关系不能继承(见继承章节)
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
在上面的代码中,定义了两个类A和B,在类B中声明了类A为B的友元类,则A类中可以访问B中的成员
Note
注意,尽管A类是B类的友元类,但是B类不可以访问A类中的成员,即单向传递
Tip
友元类的声明规则:当某个类想使用另一个类中的成员时,就在另一个类中声明友元类,例如在上面的代码中,A类想使用B类的成员,则A类在B类声明友元类(即想用哪一个类就在哪个类声明友元类)
内部类¶
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。
Note
内部类就是外部类的友元类,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元,即外部类不可以访问内部类成员
内部类特点:
- 内部类可以定义在外部类的
public
、protected
、private
都是可以的(若内部类具有private属性,则不能使用外部类::内部类 对象名
的方式创建对象) - 注意内部类可以直接访问外部类中的
static
成员,不需要外部类的对象/类名 sizeof(外部类)=外部类
,和内部类没有任何关系- 内部类虽然与外部类独立,但是内部类的生命周期还是外部类域
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
在上面的代码中,外部类outer
中有一个inner
内部类,该内部类中可以通过外部类的对象o访问外部类的成员,而对于外部类的静态成员则可以直接访问
Note
注意,内部类中不可以创建外部类对象
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
因为内部类和外部类是两个单独的类,所以在使用sizeof
计算外部类大小时不会包括内部类的大小
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
匿名对象¶
所谓匿名对象,即类对象在实例化时不给名称
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
在上面的代码中,创建了两个对象,一个对象是普通对象,普通对象的生命周期为当前函数栈帧空间销毁之前,另外一个对象是匿名对象,匿名对象的生命周期为所在行,当匿名对象执行完所在行之后就会被销毁
Note
注意:匿名对象与常量一样,当给自定义类型的引用变量时需要有const
但是如果使用一个常量引用接受匿名对象时,匿名对象不会在当前行执行完毕后销毁
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
|
在上面的代码中,对于无常量引用的匿名对象来说,当前行执行完毕后将会调用析构函数进行销毁,而常量引用的匿名对象则是在准备出当前作用域时销毁
拷贝对象时编译器做出的一些优化¶
Note
下面的优化结果由编译器决定,不同的编译器优化结果可能不同,视具体情况而定
参数传递优化¶
-
在前面的
explicit
关键字部分提到过编译器会对在单行的构造+拷贝构造优化为构造C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
#include <iostream> using namespace std; class test { private: int _num; public: test(int num) :_num(num) {} void print() { cout << _num << endl; } }; int main() { test t = 1; t.print(); return 0; } 输出结果: 1
-
在给函数形参传递实参时,如果直接传递已经创建的对象时,编译器不会对其进行优化
C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
#include <iostream> using namespace std; class test { private: int _num; public: test(int num) :_num(num) { cout << "构造函数" << endl; } void print() { cout << _num << endl; } test(const test& t) { _num = t._num; cout << "拷贝构造函数" << endl; } ~test() { cout << "析构函数" << endl; } }; void func1(const test t) { cout << "func1" << endl; } int main() { test t1(1); func1(t1); return 0; } 输出结果: 构造函数 拷贝构造函数 func1 析构函数 析构函数
在上面的代码中,首先
test
类创建了一个对象为t1
,此时调用构造函数,当t1
作为函数实参传递给func1
函数,此时会调用拷贝构造函数将t1
对象拷贝给形参t
,接着进入func1
函数栈帧空间执行func1
函数体的语句,当func1
函数结束执行后调用析构函数销毁形式参数对象t
,最后调用析构函数销毁局部对象t1
对上面的代码进行改进,直接传递整型1给
func1
函数,如下面代码C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
#include <iostream> using namespace std; class test { private: int _num; public: test(int num) :_num(num) { cout << "构造函数" << endl; } void print() { cout << _num << endl; } test(const test& t) { _num = t._num; cout << "拷贝构造函数" << endl; } ~test() { cout << "析构函数" << endl; } }; void func1(const test t) { cout << "func1" << endl; } int main() { func1(1); return 0; } 输出结果: 构造函数 func1 析构函数
在上面的代码中,直接将1作为对象传递给自定义类型的形参
t
时,常规的步骤为:调用构造函数用整型1初始化一个临时对象,再调用拷贝构造函数将临时对象中的内容拷贝给形参对象,但是此处编译器会对其进行优化为直接调用构造函数,用整型1初始化形参对象t
同理,使用匿名对象作为实际参数传递给自定义类型的形参时,编译器也会有所优化
C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
#include <iostream> using namespace std; class test { private: int _num; public: test(int num) :_num(num) { cout << "构造函数" << endl; } void print() { cout << _num << endl; } test(const test& t) { _num = t._num; cout << "拷贝构造函数" << endl; } ~test() { cout << "析构函数" << endl; } }; void func1(const test t) { cout << "func1" << endl; } int main() { func1(test(2)); return 0; } 输出结果: 构造函数 func1 析构函数
在上面的代码中,使用整型2创建了一个匿名对象,常规步骤为:调用构造函数使用整型2创建匿名对象,接着调用拷贝构造函数将匿名对象中的内容拷贝给形式参数,但是编译器优化为直接使用整型2为形式参数初始化
但是如果函数的形式参数为引用时,则不会有任何优化,直接调用构造函数进行初始化对象再由自定义类型的引用形参接收实参对象的地址
Note
注意:使用引用传参时一定要在形式参数处加
const
修饰C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
#include <iostream> using namespace std; class test { private: int _num; public: test(int num) :_num(num) { cout << "构造函数" << endl; } void print() { cout << _num << endl; } test(const test& t) { _num = t._num; cout << "拷贝构造函数" << endl; } ~test() { cout << "析构函数" << endl; } }; void func1(const test& t) { cout << "func1" << endl; } int main() { test t1(1); func1(t1); cout << endl; func1(1); cout << endl; func1(test(1)); return 0; } 输出结果: 构造函数 func1 构造函数 func1 析构函数 构造函数 func1 析构函数 析构函数
返回值优化¶
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
|
-
当调用的函数有返回对象时,使用该对象初始化对象
C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
#include <iostream> using namespace std; class test { private: int _num; public: test(int num) :_num(num) { cout << "构造函数" << endl; } void print() { cout << _num << endl; } test(const test& t) { _num = t._num; cout << "拷贝构造函数" << endl; } test& operator=(const test& t) { cout << "赋值运算符重载函数" << endl; if (this != &t) { _num = t._num; } return *this; } ~test() { cout << "析构函数" << endl; } }; test func() { cout << "func" << endl; test t(1); return t; } int main() { test t1 = func(); return 0; }
在上面的代码中,使用
func
函数的返回值初始化t1
对象,常规的过程为:调用拷贝构造函数将func
函数的返回值放入一个自定义类型的临时变量中,再通过拷贝构造函数将临时变量中的内容拷贝给t1
对象,但是这里编译器会优化为调用一个构造函数将func
的返回值作为初始化值直接初始化t1
对象但是如果将两个步骤分开,如下面的代码
C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
#include <iostream> using namespace std; class test { private: int _num; public: test() :_num() { cout << "构造函数" << endl; } void print() { cout << _num << endl; } test(const test& t) { _num = t._num; cout << "拷贝构造函数" << endl; } test& operator=(const test& t) { cout << "赋值运算符重载函数" << endl; if (this != &t) { _num = t._num; } return *this; } ~test() { cout << "析构函数" << endl; } }; test func() { cout << "func" << endl; test t; return t; } int main() { test t1; t1 = func(); return 0; }
在上面的代码中,因为
t1
对象需要完成实例化,所以会调用构造函数,接着执行t1 = func()
语句,因为赋值运算符有从右往左的结合性,所以先执行func
函数,在func
函数中会再次调用构造函数创建一个对象,(注意中间有一个过程为:调用拷贝构造将返回对象拷贝到临时对象中,再调用析构函数销毁局部对象t
)此时执行赋值语句,此时调用赋值运算符重载函数,将t
对象的内容给t1
对象 -
当返回的是匿名对象,使用该匿名对象初始化对象
C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
#include <iostream> using namespace std; class test { private: int _num; public: test() :_num() { cout << "构造函数" << endl; } void print() { cout << _num << endl; } test(const test& t) { _num = t._num; cout << "拷贝构造函数" << endl; } test& operator=(const test& t) { cout << "赋值运算符重载函数" << endl; if (this != &t) { _num = t._num; } return *this; } ~test() { cout << "析构函数" << endl; } }; test func() { cout << "func" << endl; return test(); } int main() { test t1 = func(); return 0; } 输出结果: func 构造函数 析构函数
在上面的代码中,先执行
func
函数,常规步骤为:执行test
类中的构造函数创建一个匿名对象,接着调用拷贝构造将匿名对象拷贝到临时对象中返回,接着调用拷贝构造将返回值拷贝给t1
对象,但是此处编译器会优化为直接用返回的匿名对象的内容作为初始值初始化对象t1
总结:
- 为了编译器更好得优化,在传参数时,可以考虑使用引用变量作为参数
- 当使用到返回值时,如果能用引用就使用引用,不能使用引用需要返回值时,可以考虑返回匿名对象