一、类
1.构造函数有以下类型。
- 普通构造函数:
- 复制构造函数(拷贝构造函数):用另一个对象来初始化对象对应的内存
class CPPTest |
- 移动构造函数:也是用另一个对象来初始化对象。
- 默认构造函数:当类没有任何构造函数时,编译期会为该类生成一个默认的的构造函数,在最普通的类中,默认构造函数什么都没做,对象对应的内存没有被初始化。
2.析构函数:
析构函数介绍:当类对象被销毁时,就会调用析构函数。
- 栈上对象的销毁时机就是函数栈销毁时。
- 堆上的对象销毁时机就是该堆内存被手动释放时,如果用new申请的这块堆内存,那调用delete销毁这块内存时就会调用析构函数。
二、this,常成员函数与常对象
1.this关键字:
(1) this是什么:
- 编译器将this解释为指向函数所作用的对象的指针。C++类的本质就是C语言的结构体外加几个类外的函数,C++最后都要转化为C语言来实现,类外的函数就是通过this来指向这个类的。
#include <iostream> |
- 当然,这么说并非完全准确,this是一个关键字,只是我们将它当做指针理解罢了。
this有很多功能是单纯的指针无法满足的。比如每个类函数的参数根本没有名 叫this的指针。这不过是编译器赋予的功能罢了。
- 常成员函数和常对象
首先说一下:常成员函数和常对象很多人并不在意,确实,都写普通变量也可以。但是,我还是要提一点,在大型程序中,尽量加上const关键字可以减少很多不必要的错误。
(1) const关键字含义:
常成员函数就是无法修改成员变量的函数。可以理解为将this指针指向对象用const修饰的函数。
其实就是在类成员函数后面加上const关键字。
常对象就是用const修饰的对象,定义好之后就再也不需要更改成员变量的值了。常对象在大型程序中还是很有意义的。
#include <iostream> |
(2) 常成员函数注意事项:
因为类的成员函数已经将this指针省略了,只能在函数后面加const关键字来实现无法修改类成员变量的功能了。
- 注意:常函数无法调用了普通函数,否则常函数的这个“常”字还有什么意义。
- 成员函数能写作常成员函数就尽量写作常成员函数,可以减少出错几率。
- 同名的常成员函数和普通成员函数是可以重载的,常量对象会优先调用常成员函数,普通对象会优先调用普通成员函数
三、inline,mutable,default,delete
- inline关键字
(1) inline关键字的有什么作用:
- 在函数声明或定义中函数返回类型前加上关键字inline就可以把函数指定为内联函数。关键字inline必须与函数定义放在一起才能使函数成为内联,仅仅将inline放在函数声明前不起任何作用。
- 内联函数的作用,普通函数在调用时需要给函数分配栈空间以供函数执行,压栈等操作会影响成员运行效率,于是C++提供了内联函数将函数体放到需要调用函数的地方,用空间换效率。不再创建新的栈来执行函数
(2) inline关键字的注意事项:inline关键字只是一个建议,开发者建议编译器将成员函数当做内联函数,一般适合搞内联的情况编译器都会采纳建议。
(3) Inline关键字的总结。使用inline关键字就是一种提高效率,但加大编译后文件大小的方式,现在随着硬件性能的提高,inline关键字用的越来越少了。
#include <iostream> |
- mutable关键字
(1) mutable关键字的作用:
- Mutable意为可变的,与const相对,被mutable修饰的成员变量,永远处于可变的状态,即便处于一个常函数中,该变量也可以被更改。
这个关键字在现代C++中使用情况并不多,一般来说只有在统计函数调用次数时才会用到。
(2) mutable关键字的注意事项
- mutable是一种万不得已的写法,一个程序不得不使用mutable关键字时,可以认为这部分程序是一个糟糕的设计。
- mutable不能修饰静态成员变量和常成员变量。
(3) 总结:mutable关键字是一种没有办法的办法,设计时应该尽量避免,只有在统计函数调用次数这类情况下才推荐使用。这个关键字也称不上是重点。
#include <iostream> |
- default关键字
(1) default关键字的作用:default关键字的作用很简单。
- 在编译时不会生成默认构造函数时便于书写。
- 也可以对默认复制构造函数,默认的赋值运算符和默认的析构函数使用,表示使用的是系统默认提供的函数,这样可以使代码更加明显。
- 现代C++中,哪怕没有构造函数,也推荐将构造函数用default关键字标记,可以让代码看起来更加直观,方便。
总结:default关键字还是推荐使用的,在现代C++代码中,如果需要使用一些默认的函数,推荐用default标记出来。
#include <iostream> |
- delete关键字
(1) Delete关键字的作用:C++ 会为程序生成默认构造函数,默认复制构造函数,默认重载赋值运算符。在很多情况下,我们并不希望这些默认的函数被生成,在C++11以前,只能有将此 函数声明为私有函数或是将函数只声明不定义两种方式。
C++11于是提供了delete关键字,只要在函数最后加上“=delete”就可以明确告诉 编译期不要默认生成该函数。
总结:delete关键字还是推荐使用的,在现代C++代码中,如果不希望一些函数默认生成,就用delete表示,这个功能还是很有用的,比如在单例模式中,
#include <iostream> |
四、友元类与友元函数
- 友元的介绍:友元就是可以让另一个类或函数访问私有成员的简单写法。
#include <iostream> |
- 注意:
- 友元会破坏封装性,一般不推荐使用,所带来的方便写几个接口函数就解决了。
- (*)某些运算符的重载必须用到友元的功能,这才是友元的真正用途。
- 总结:友元平常并不推荐使用,新手不要再纠结友元的语法了,只要可以用友元写出必须用友元的重载运算符就可以了。
五、重载运算符
- 重载运算符的作用:
- 很多时候我们想让类对象也能像基础类型的对象一样进行作基础操作,比如“+”,“-”,“*”,“\”,也可以使用某些运算符“=”,“()”,“[]”,“<<”,“>>”。但是一般的类即使编译器可以识别这些运算符,类对象也无法对这些运算符做出应对,我们必须对类对象定义处理这些运算符的方式。
- C++提供了定义这些行为的方式,就是“operator 运算符”来定义运算符的行为,operator是一个关键字,告诉编译器我要重载运算符了。
- 注意:
- 我们只能重载C++ 已有的运算符,所有无法将 “ ” 这个运算符定义为指数的形式,因为C++ 根本没有 “ ” 这个运算符。
- C++ 重载运算符不能改变运算符的元数,“元数”这个概念就是指一个运算符对应的对象数量,比如“+”必须为“a + b”,也就是说“+”必须有两个对象,那么“+”就是二元运算符。比如“ ++ ”运算符,必须写为“ a++ ”,也就是一元运算符。
- 重载运算符举例,以下全部用代码演示:
- 一元运算符重载
- “++”,“–”,
- “[]”
- “()”
- “<<”,“>>”
- 二元运算符重载
- “+”,“-”,“*”,“/”
- “=”,
- “>”,“<”,“==”
#include <iostream>
#include <vector>
class CPPTest
{ //重载输出流,必须使用友元
friend std::ostream &operator<<(std::ostream &os, const CPPTest &cpptest);
friend std::istream &operator>>(std::istream &is, CPPTest &cpptest); //重载输入流
public:
void operator++() //++
{
++old;
}
void operator--() //--
{
--old;
}
void operator()() //()
{
std::cout << "hello world" << std::endl;
}
void operator()(const std::string &name) //可重载
{
std::cout << name << std::endl;
}
int operator[](unsigned i) //[]
{
if (i > 0 && i < ivec.size())
{
return ivec[i];
}
return -1;
}
CPPTest operator+(const CPPTest &CPPTest)
{ //重载+,不能返回引用,因为返回的是对象
old += CPPTest.old;
return *this;
}
bool operator<(const CPPTest &cpptest) //重载<
{
return this->old < cpptest.old ? true : false;
}
bool operator>(const CPPTest &cpptest) //重载>
{
return this->old > cpptest.old ? true : false;
}
bool operator==(const CPPTest &cpptest) //重载==
{
return this->old == cpptest.old ? true : false;
}
CPPTest &operator=(const CPPTest &CPPTest) //重载=,必须返回引用,因为返回的就是自己本身
{
if (this == &CPPTest)
{
return *this;
}
else
{
old = CPPTest.old;
name = CPPTest.name;
ivec = CPPTest.ivec;
return *this;
}
}
unsigned old = 0;
std::vector<int> ivec{1, 2, 3, 4, 5, 6};
std::string name;
};
std::ostream &operator<<(std::ostream &os, const CPPTest &cpptest)
{
os << cpptest.old << std::endl;
return os;
}
std::istream &operator>>(std::istream &is, CPPTest &cpptest)
{
is >> cpptest.name;
return is;
}
int main()
{
CPPTest test;
++test; //重载++
--test; //重载--
std::cout << test.old << std::endl;
std::cout << test[3] << std::endl; //重载[]
test();
test("sssss");
std::cin >> test;
std::cout << test;
CPPTest test2;
test2.old = 200;
CPPTest test3;
test3 = test + test2; //重载+
std::cout << test3.old << std::endl; //重载[]
test = test2; //重载=
return 0;
}
至于唯一的三元运算符“?:”,不能重载
- 类类型转化运算符:“operator 类型”
- 特殊的运算符:new,delete,new[],delete[]
注意:“=”类会默认进行重载,如果不需要可以用“delete关键字进行修饰”。
总结:重载运算符非常重要,C++类中几乎都要定义各种各种的重载运算符。
六、普通继承及其实现原理
- C++继承介绍:
C++非继承的类相互是没有关联性的,假设现在需要设计医生,教师,公务员三个类,需要定义很多重复的内容而且相互没有关联,调用也没有规律。如果这还算好,那一个游戏有几千件物品,调用时也要写几千个函数。这太要命了。于是继承能力就应运而生了。
- C++继承原理:
C++的继承可以理解为在创建子类成员变量之前先创建父类的成员变量,实际上,C语言就是这么模仿出继承功能的。
- C++继承的注意事项。
- C++子类对象的构造过程。先调用父类的构造函数,再调用子类的构造函数,也就是说先初始化父类的成员,再初始化子类的成员。
- 若父类没有默认的构造函数,子类的构造函数又未调用父类的构造函数,则无法编译。
- C++子类对象的析构过程。先调用父类的析构函数,再调用子类的析构函数。
#include <iostream> |
总结:面向对象三大特性的继承就这么简单,很多人觉得类继承很复杂,其实完全不是这样的,只要明白子类在内存上其实就相当于把父类的成员变量放在子类的成员变量前面罢了。构造和析构过程也是为了这个机制而设计的。
七、虚函数及其实现原理,override关键字
- 虚函数介绍:
- 虚函数就是面向对象的第三大特点:多态。多态非常的重要,它完美解决了上一课设计游戏装备类的问题,我们可以只设计一个函数,函数参数是基类指针,就可以调用子类的功能。比如射击游戏,所有的枪都继承自一个枪的基类,人类只要有一个开枪的函数就可以实现所有枪打出不同的子弹。
- 父类指针可以指向子类对象,这个是自然而然的,因为子类对象的内存前面就是父类成员,类型完全匹配。(不要死记硬背,尽量理解原理)
- 当父类指针指向子类对象,且子类重写父类某一函数时。父类指针调用该函数,就会产生以下的可能
- 该函数为虚函数:父类指针调用的是子类的成员函数。
- 该函数不是虚函数:父类指针调用的是父类的成员函数。
- 虚函数的注意事项:
- 子父类的虚函数必须完全相同,为了防止开发人员一不小心将函数写错,于是C++11添加了override关键字。
- (*) 父类的析构函数必须为虚函数:这一点很重要,当父类对象指向子类对象时,容易使独属于子类的内存泄露。会造成内存泄露的严重问题。
#include <iostream> |
- 虚函数实现多态的原理介绍
- 动态绑定和静态绑定:
- 静态绑定:程序在编译时就已经确定了函数的地址,比如非虚函数就是静态绑定。
- 动态绑定:程序在编译时确定的是程序寻找函数地址的方法,只有在程序运行时才可以真正确定程序的地址,比如虚函数就是动态绑定。
- 虚函数是如何实现动态绑定的呢?
- 每个有虚函数的类都会有一个虚函数表,对象其实就是指向虚函数表的指针,编译时编译器只告诉了程序会在运行时查找虚函数表的对应函数。每个类都会有自己的虚函数表,所以当父类指针引用的是子类虚函数表时,自然调用的就是子类的函数。
八、静态成员变量与静态函数
- 静态成员变量:
- 静态成员变量,在编译期就已经在静态变量区明确了地址,所以生命周期为程序从开始运行到结束,作用范围为与普通的成员变量相同。这些对于类的静态成员变量同样适用。
- 类的静态成员变量因为创建在静态变量区,所以直接属于类,也就是我们可以直接通过类名来调用,当然通过对象调用也可以。
#include <iostream> |
- 静态成员变量的注意项:
- 静态成员变量必须在类外进行初始化,否则会报未定义的错误,不能用构造函数进行初始化。因为静态成员变量在静态变量区,只有一份,而且静态成员变量在编译期就要被创建,成员函数那都是运行期的事情了
- 静态成员函数的特点:静态成员函数就是为静态成员变量设计的,就是为了维持封装性。 和普通成员函数没有区别,都在代码区。
#include <iostream> |
九、(*)纯虚函数
- 纯虚函数介绍:
任何一个成员函数后面加上=0,该函数就变成了纯虚函数,可以不用实现,该函数所在的类就会变成虚基类,无法产生对象
所以纯虚函数的语法诞生了,只要将一个虚函数写为纯虚函数,那么该类将被认为无实际意义的类,无法产生对象。纯虚函数也不用去写实际部分。写了编译期也会自动忽略。
#include <iostream> |
十、RTTI
- RTTI介绍:
- RTTI(Run Time Type Identification)即通过运行时类型识别,程序能够通过基类的指针或引用来检查这些指针或引用所指向的对象的实际派生类。
- C++ 为了支持多态,C++ 的指针或引用的类型可能与它实际指向对象的类型不相同,这时就需要rtti去判断类的实际类型了,rtti是C++判断指针或引用实际类型的唯一方式。
- RTTI的使用场景:
- 异常处理:这是RTTI最主要的使用场景
- IO操作
- RTTI的使用方式:RTTI的使用过程就两个函数
typeid函数:typeid函数返回的一个叫做type_info的结构体,该结构体包括了所指向对象的实际信息,其中name()函数就可以返回函数的真实名称。type_info结构体其他函数没什么用.
dynamic_cast函数:C++提供的将父类指针转化为子类指针的函数。
#include <iostream> |
- RTTI的注意事项:
- 当使用typeid函数时,父类和子类必须有虚函数(父类有了虚函数,子类自然会有虚函数),否则类型判断会出错。
- RTTI总结:就是C++在运行阶段判断对象实际类型的唯一方式。
十一、多继承
- 多继承的概念
- 就是一个类同时继承多个类,在内存上,该类对象前面依次为第一个继承的类,第二个继承的类,依次类推。
#include <iostream> |
- 多继承的注意点:
- 多继承最需要注意的点就是重复继承的问题
- 多继承会使整个程序的设计更加复杂,平常不推荐使用。C++ 语言中用到多继承的地方主要就是接口模式。相较于C++ ,java直接取消了多继承的功能,添加了接口。
- 多继承的总结:多继承这个语法虽然在某些情况下使代码写起来更加简洁,但会使程序更加复杂难懂,一般来说除了接口模式不推荐使用。
十二、虚继承及其实现原理
- 虚继承的概念:虚继承就是为了避免多重继承时产生的二义性问题。虚继承的问题用语言不好描述,但用代码非常简单,所以直接写代码了。
#include <iostream> |
- 虚继承的实现原理介绍:
- 使用了虚继承的类会有一个虚继承表,表中存放了父类所有成员变量相对于类的偏移地址。
- 按照刚才的代码,B1,B2类同时有一个虚继承表,当C类同时继承B1和B2类时,每继承一个就会用虚继承表进行比对,发现该变量在虚继承表中偏移地址相同,就只会继承一份。
虚继承的注意点:没什么需要注意的,语法简单。
虚继承的总结:这个语法就是典型的语法简单,但在游戏开发领域经常使用的语法,其它领域使用频率会低很多。
十三、(**)移动构造函数与移动赋值运算符
- 对象移动的概念:
- 对一个体积比较大的类进行大量的拷贝操作是非常消耗性能的,因此C++11中加入了“对象移动”的操作
- 所谓的对象移动,其实就是把该对象占据的内存空间的访问权限转移给另一个对象。比如一块内存原本属于A,在进行“移动语义”后,这块内存就属于B了。
- 移动语义为什么可以提高程序运行效率。因为我们的各种操作经常会进行大量的“复制构造”,“赋值运算”操作。这两个操作非常耗费时间。移动构造是直接转移权限,这是不是就快多了。
#include <iostream>
#include <cstring>
class Test
{
public:
Test() = default;
Test(const Test &test)
{
if (test.str)
{
str = new char[strlen(test.str) + 1]();
strcpy_s(str, strlen(test.str) + 1, test.str);
}
else
{
str = nullptr;
}
}
/**
* @brief Construct a new Test object
* 移动构造函数
* @param test
*/
Test(Test &&test)
{
if (test.str)
{
//直接转移权限
str = test.str;
test.str = nullptr; //需要修改 形参对象,因此不能对形参加const
}
else
{
str = nullptr;
}
}
Test &operator=(const Test &test)
{
if (this == &test)
{
return *this;
}
//判断str是否为空
if (str)
{
delete[] str;
str = nullptr;
}
//判断形参对象是否是空字符串
if (test.str)
{
str = new char[strlen(test.str) + 1]();
strcpy_s(str, strlen(test.str) + 1, test.str);
}
else
{
str = nullptr;
}
return *this;
}
Test &operator=(Test &&test)
{
if (this == &test)
{
return *this;
}
//判断str是否为空
if (str)
{
delete[] str;
str = nullptr;
}
if (test.str)
{
str = test.str;
test.str = nullptr;
}
else
{
str = nullptr;
}
return *this;
}
private:
char *str = nullptr;
};
Test makeTest()
{
Test t;
return t;
}
int main()
{
Test t=makeTest();
return 0;
}
注意:在进行转移操作后,被转移的对象就不能继续使用了,所以对象移动一般都是对临时对象进行操作(因为临时对象很快就要销毁了)。
注意这里的右值引用不能是const的,因为你用右值引用函数参数就算为了让其绑定到一个右值上去的!就是说这个右值引用是一定要变的,但是你一旦加了const就没法改变该右值引用了。
- 默认移动构造函数和默认移动赋值运算符
会默认生成移动构造函数和移动赋值运算符的条件:
- 只有一个类没有定义任何自己版本的拷贝操作(拷贝构造,拷贝赋值运算符),且类的每个非静态成员都可以移动,系统才能为我们合成。
- 可以移动的意思就是可以就行移动构造,移动赋值。所有的基础类型都是可以移动的,有移动语义的类也是可以移动的。