1.内存分区模型
在一个程序中,系统会自动将我们的程序进行分区管理,分为四个区:代码区、全局区、栈区、堆区,这四个区域各有分工,共同撑起我们程序的一片天,下面让我们一起研究一下它们。
意义:赋予不同数据生命周期,给我们更大的灵活编程
1.1 未执行程序之前
在未执行程序之前我们的代码会分为两个区域:代码区和全局区。
代码区:存放函数体的二进制代码,有操作系统进行管理。
简单来说,代码区存放的就是我们在编译器上书写的代码,像文本文件一样,代码也占内存,于是需要一个区专门来存放它。
我们只需知道代码区的两个特点就可以了:
- 共享性:方便程序反复执行
- 只读性:防止意外修改指令
全局区:存放全局变量和静态变量以及常量,该区域的数据在程序结束后由系统释放
下面简单介绍一下这三个变量都是些什么牛马。
1.1.1 全局变量
全局变量是定义在函数外的变量,顾名思义:整个代码从头到尾每个函数都能读取它,也能使用它。
示例如下:
//此处创建的变量c在函数体外,为全局变量
int c = 30;
int main(){}
1.1.2 静态变量
静态变量是在计算机编程领域指在程序执行前系统就为之静态分配(也即在运行时中不再改变分配情况)存储空间的一类变量。
语法:static 数据类型 变量名;
示例如下:
static int a = 10;
1.1.3 常量
不是所有的常量都存放在全局区,只有字符串常量与全局常量才会存放在全局区,而常数不会存放在全局区。
下面我们分别打印输出他们的地址,具体看一下他们的地址编号:
//此处a和b都在main函数体内,为局部变量
int a = 10;
int b = 20;
cout << "a的地址为:" << (int)&a << endl;
cout << "b的地址为:" << (int)&b << endl;
cout << "c的地址为:" << (int)&c << endl;
//静态变量:在普通变量前加static关键字
static int d = 40;
cout << "d的地址为:" << (int)&d << endl;
//常量
cout << "字符串常量的地址为:" << (int)&"JYP" << endl;
结果如下:
在输出的结果中,我们可以看到,局部变量a、b的地址挨得很近都是85开头,而全局变量c、静态变量d、字符串常量的地址挨得很近都是87开头,这个结果不出所料。
1.2 程序执行后
在程序执行后分为两个区域:栈区和堆区
栈区:由编译器自动分配释放,存放函数参数值和局部变量
函数参数值与局部变量之前介绍过,它们的生命周期只是在函数内部,它们只是一个傀儡,在函数结束后它们就会被无情地释放,所以要注意一点,我们在函数中不要返回一个局部变量的地址,不然会出现错误。
示例如下:
int* func()
{
int p = 10;
return &p;
}
int main()
{
//接受func函数的返回值
int* p = func();
cout << *p << endl;//第一次可以打印正确的数字,是因为编译器做了一次保留
cout << *p << endl;//第二次这个数据就不再保留了
}
结果如下:
我们可以看到,第一次打印输出是正确的显示了局部变量的值得,这是因为编译器怕我们会出现这种错误,所以为我们做了一次保留,但是第二次再打印输出它,我们就会发现它出现了乱码,这个指针p指向了一块莫名其妙的内存区域。
那么,我们有什么办法可以改善这种情况呢?当然有!
堆区:由程序员分配和释放,若不释放,程序结束时由操作系统自动回收。
在C++中主要利用new关键字在堆中开辟内存,这相当于C语言中的malloc关键字,相应的我们可以用delete关键字去释放这块内存,这相当于C语言中的free关键字。
分配内存语法:new 数据类型(数值);
释放内存语法:delete 变量名;
示例如下:
int* fund()
{
//利用new关键字,可以将数据开辟到堆区
int *a = new int(10);
return a;
}
int main()
{
//在堆区开辟数据
int* q = fund();
cout << *q << endl;
cout << *q << endl;
}
结果如下:
可以看到,两次都正确打印出了10这个值。
我们使用new关键字开辟内存后,定义的这个指针本身是局部变量,放在栈区,但是指针存储的数据放在了堆区,也就是说在这个函数结束后,指针p会被释放,但是我们自己分配的这块内存却存在了堆区,存放了10这个数据!
下面我们看一看释放内存delete关键字的使用:
int* func()
{
int * p = new int(10);//指针本身是局部变量,放在栈区,但是指针存储的数据放在了堆区
return p;
}
void test01()
{
int* p = func();
cout << *p << endl;
delete p;
//cout << *p << endl;//内存已经被释放,再次访问为非法操作
}
如果我们执行注释那行代码,编译器就会报错,因为我们已经使用了delete关键字释放了这块内存。
结果如下:
最后补充一点:我们也可以使用new关键字创建一个数组(一块连续的内存)
语法:new 数据类型[数组长度];
这样定义返回的是内存空间的首地址,所以我们也是使用一个指针去接收这段内存。
当我们想访问它时,可以按照指针解引用的语法去做,也可以直接使用“指针名[i]”这种方式。
示例如下:
void fun()
{
int* arr = new int[10];
for (int i = 0; i < 10; i++)
{
*(arr + i) = i + 100;//解引用的方式
}
//delete[] arr;
for (int i = 0; i < 10; i++)
{
cout << arr[i] << endl;//数组方式
}
}
结果如下:
在释放这块数组内存时,也是使用关键字delete,但语法稍有不同,如代码片段中的注释部分,中间要加个中括号,我就不展开讲了,懒。
语法:delete[ ] 数组名;
2.引用
2.1 概述
作用:给变量起别名,可以用别名去操作这块内存
语法:数据类型 &别名 = 原名;
示例如下:
int a = 10;
int& b = a;
cout << "b= " << b << endl;
b = 30;
cout << "a= " << a << endl;
结果如下:
可以看到,我们对b进行了赋值操作,但是a的值也随之改变,相当于a与b进行了绑定。
注意事项:
1.引用时必须初始化
int &b;//这句话是错的!
2.引用在初始化后,不可改变
int &b = a;
&b = c;//这句话是错的,引用初始化后,不能改变!
为什么引用在初始化后就不能改变了呢,这就要追寻到很多年前了…
引用的本质:在C++内部实现一个指针常量
也就是说:int &b=a;这句话,等价于 int * const b=a;由我们之前学的知识可以知道,指针常量一旦指向了一个值后,它的数值是可以改变的,但是它的指向不能改变,这也就是引用在初始化后不可改变的原因。
2.2 引用做函数参数
作用:函数传参时,可以利用引用的技术实现形参修饰实参
示例如下:
void swap1(int& a, int& b)
{
//这是一个交换值得函数
int temp = a;
a = b;
b = temp;
}
int main()
{
int e = 30;
int f = 40;
swap1(e, f);
cout << "c= " << e << endl;
cout << "d= " << f << endl;
}
我们知道,如果不传入实参地址,并使用指针接收,对指针进行操作的话,在主函数中的实参数值并不会改变,但是我们上面说了,引用的本质就是指针常量,也就是一个指针,我们利用引用这个特性,就可以极大的简化我们的代码,避免使用指针的麻烦!
结果如下:
2.3 引用做函数返回值
引用可以作为函数参数,当然也可以作为函数的返回值,但是引用作为函数返回值时需要注意一点:不要返回局部变量的引用,但是可以返回静态变量的引用。
我们可以这样理解,如果一个我们定义了一个局部变量的引用,但是当函数结束后,这个局部变量被系统释放了,我们的引用(常量指针)找不到它指向的那块地址了,就会出现乱码错误。
实例如下:
int& test02()
{
static int a = 10;
return a;
}
int main() {
int ref = test02();
test02() = 1000;
cout << "ref = " << ref << endl;
system("pause");
return 0;
}
代码实例中,ref为静态变量a的引用,并且注意到在第9行代码中,我们将函数test02()设置为了左值,进行了赋值操作,相应的ref与a的值都会发生改变。
结果如下:
2.4 常量引用
提到常量这两个字,我们很自然的就会想到const关键字,这里的常量引用也是一个道理。
作用:用const修饰形参,防止误操作
实例…太简单了,没有实例,与前文类比即可。
注意一点: 引用必须引一块合法内存。
int &ref = 10;//这句话是错的,因为10不是一个变量原名
const &ref = 10;//这句话是对的,我也不知道编译器怎么做的,反正这样写,编译器会自动创建一个临时变量temp,这句话就会等价为:int temp = 10;const int &ref = temp;
3.函数提高
3.1 函数默认参数
语法:返回值类型 函数名(参数=默认值){语句}
解释:设置默认参数后,若传入值则会代替默认值,若没有设置返回值,则会使用默认值
示例如下:
void test(int a, int b=10)
{
cout << "b=" << b << endl;
}
int main(
{
test(30, 50);//给b传入一个值
}
结果如下:
可以看到,test函数中的默认值被改成了我们传入的值,而我们如果不给他传入一个值,结果如下:
void test(int a, int b=10)
{
cout << "b=" << b << endl;
}
int main(
{
test(30);//给b传入一个值
}
b则会等于我们给它的默认值。
下面是两点注意事项:
①若某位置已经有了默认参数,那么此位置往后,从左到右都必须有默认值。
void test(int a=10, int b)//这样定义默认值是错的!
{
cout << "b=" << b << endl;
}
②如果函数声明有默认参数,函数实现就不能有默认参数,否则会出现重定义的情况。
示例如下:
void test(int a = 10, int b = 10);
void test(int a=10, int b=10)
{
cout << "b=" << b << endl;
}
结果如下:
3.2 函数占位参数
说明:C++中函数的形参列表里可以有占位参数,用来做占位,调用时必须填补该位置。
语法:返回值类型 函数名(数据类型){语句}
示例如下:
void fun(int )
{
cout << "我巨帅!!" << endl;
}
int main() {
int a = 10;
fun(a);
system("pause");
return 0;
}
结果如下:
占位参数也可以有默认值,但是我们虽然给占位参数传入了一个值,却没有一个变量去接它,因此我们也不知道怎么使用它,关于它的使用,可能我现在还接触不到那个级别,那就以后再说嘛。
3.3 函数重载
作用:函数名可以相同,提高函数复用性
函数重载需要满足以下三个条件:
- 在同一个作用域下
- 函数名称相同
- 函数参数类型不同、或个数不同、或顺序不同
示例如下:
void fun()
{
cout << "我太帅了!" << endl;
}
void fun(int )
{
cout << "我巨帅!!" << endl;
}
int main() {
int a = 10;
//fun();
fun(a);
system("pause");
return 0;
}
如果我们用代码中写的fun(a)的方式去调用函数,我们将调用第二个函数,因为它们的参数列表不一样,第二个中有一个占位参数,系统会自动识别它们的不同,为我们提供正确的函数调用。
结果如下:
如果我们不传入任何参数,系统会调用第一个函数。
示例如下:
void fun()
{
cout << "我太帅了!" << endl;
}
void fun(int )
{
cout << "我巨帅!!" << endl;
}
int main() {
int a = 10;
fun();
system("pause");
return 0;
}
结果如下:
注意一点: 函数的返回值类型不能作为函数重载的条件
示例如下:
void fun()
{
cout << "我太帅了!" << endl;
}
int fun()
{
cout << "我巨帅!!" << endl;
}
结果如下:
在注意两个坑:
- 引用作为重载条件
- 函数重载遇到函数默认参数
示例如下:
void fun(int &a)
{
cout << "我太帅了!" << endl;
}
void fun(const int &a)
{
cout << "我巨帅!!" << endl;
}
int main() {
int a = 10;
fun(a);
system("pause");
return 0;
}
如上述代码所示,我们传入的是一个局部变量(在栈区),这时系统会为我们调用上面的函数,因为将a这个变量传给第二个函数是不合法的。
结果如下:
而如果,我们给fun函数传入一个常量,那情况就会变得不一样了。
示例如下:
void fun(int &a)
{
cout << "我太帅了!" << endl;
}
void fun(const int &a)
{
cout << "我巨帅!!" << endl;
}
int main() {
fun(10);
system("pause");
return 0;
}
结果如下:
传入的常量10,存放在常量区,因此用常量引用可以接收它,也可以这样理解:当传入常量10时,代码等价于const int &a = 10,上面我们说过,这句代码的意思是:int temp = 10;const int &a = temp;这样是合法的!
有默认参数的情况如下:
void fun(int a,int b=10)
{
cout << "我太帅了!" << endl;
}
void fun(int a)
{
cout << "我巨帅!!" << endl;
}
int main() {
fun(10);
system("pause");
return 0;
}
结果如下:
因为第一个函数的形参列表中有一个默认参数b,当我们只传入一个值10时,编译器不知道用哪个函数合适,好像两个函数都可以被调用,这样会出现二义性,这时系统就傻了!
4.类和对象
C++面向对象的三大特性为:封装、继承、多态
C++认为万物皆有对象,就我没有对象…对象上有其属性和行为。
具有相同性质的对线,我们可将他们抽象成类,比如人就可以抽象成一类,人都有两个胳膊,两个眼睛,这都是人的属性,人会吃饭,吃饭就是人这一类的一个行为。
4.1 封装
意义:将属性和行为作为一个整体,表现生活中的事物,将属性和行为加以权限控制
语法:class 类名{访问权限:属性/行为};
属性一般为一些变量,行为一般指的是一些函数,就相当于将一些变量和函数定义成了这个类专属的变量和函数。我们如果想使用其中的变量或函数,只需要像结构体一样使用“.”这个符号去调用即可。
定义类其实和定义结构体有很多类似之处,一般类都定义在主函数前面,而当我们定义好一个类之后,就可以在主函数中通过定义好的类,创建具体变量,这个过程叫做“实例化”。
示例如下:
class Student
{
public:
//属性
string name;
string id;
//行为
void printf()
{
cout << "姓名:" << name << endl;
cout << "学号:" << id << endl;
}
};
int main() {
Student s1;
s1.name = "K.Fire";
s1.id = "2191300311";
s1.printf();
}
结果如下:
此外,在设计类时,可以把属性和行为放在不同的权限下,加以控制,访问权限有三种:
- public:公共权限–成员类内、类外均可访问
- protected:保护权限–成员类内可以访问,类外不可以访问
- private:私有权限–成员类内可以访问,类外不可访问
我们可以看到保护权限和私有权限的作用好像是一样的,但其实他们还是有区别的,区别在于:子类可以访问父类的保护权限,而不能访问父类的私有权限,你可以理解为私有权限是“最高权限”,是个人隐私,谁也不能知道。
示例如下:
class Student
{
public:
//属性
string name;
string id;
//行为
void printf()
{
cout << "姓名:" << name << endl;
cout << "学号:" << id << endl;
}
protected:
string language = "C++";
private:
string lover;
public:
void plover()
{
lover = "Myself";
cout << "I Love " << lover << endl;
}
};
int main()
{
Student s;
cout << s.lover <<endl;
}
结果如下:
一般来说,我们会将成员属性设置为私有,然后设置一些接口,比如一些在公共权限内的输入、输出这些属性的函数去访问这些属性。
这样做的优点有:
- 将所有成员属性设置为私有,可以控制读写权限
- 对于写权限,我们可以检测数据的有效性
当然,我们的类也可以想结构体一样,进行分文件编写,在进行分文件编写时稍有不同,“老师”类的分文件编写实例如下:
头文件:
#pragma once
#include<iostream>
using namespace std;
class Teacher
{
public:
void printfName();
private:
string Tname = "张连山";
string id;
};
头文件中书写类声明和变量声明还有方法声明即可
源文件:
#include"teacher.h"
void Teacher::printfName()
{
cout << "老师的名字为:" << Tname << endl;
}
在源文件中写具体方法的代码,但这时需要使用“类名::方法”的形式去写函数名,源文件中不用再次定义变量,也不用注明权限了,因为这些事情在头文件中我们已经做了。
4.2 对象的初始化清理
对象的初始化清理,其实指的就是构造函数和析构函数。
4.2.1 构造函数
构造函数是在创建对象时,系统为我们自动执行的初始化函数,如果我们不手动设置,系统会为我们默认提供一个,但默认的这个没有任何内容,也没有任何参数。
语法:类名(){}
注意事项:
- 构造函数没有返回值,也无需写void
- 函数名与类名相同
- 构造函数可以有参数,因此可以发生重载
- 在调用对象时会自动调用构造,且只调用一次
示例如下:
class Student
{
public:
//构造函数
Student()
{
cout << "无参构造函数的调用。" << endl;
}
Student(string a)
{
name = a;
cout << "有参构造函数的调用。" << endl;
}
//拷贝构造函数
Student(const Student& s1)
{
//将传入的人身上的所有属性,都拷贝到我身上
name = s1.name;
cout << "拷贝构造函数调用。" << endl;
}
public:
//属性
string name;
string id;
//行为
void printf()
{
cout << "姓名:" << name << endl;
cout << "学号:" << id << endl;
}
protected:
string language = "C++";
private:
string lover;
public:
void plover()
{
lover = "Myself";
cout << "I Love " << lover << endl;
}
};
有的构造函数中有参数,有的构造函数中甚至还有一个类,其实这也没啥,当我们想调用它们的时候,就像调用普通函数一样,传入对应类型的参数或者类就好了。
示例如下:
Student s2;//默认构造函数调用
Student s3("张连山");//有参构造函数
Student s4(s3);//拷贝构造函数
结果如下:
<hr style=" border:solid; width:100px; height:1px;" color=#000000 size=1">
4.2.2 析构函数
析构函数就是一个这个类调用完毕,要关闭了的标志,析构函数就没有这么多参数,或者传入一个类怎么麻烦了,因为它就是一个工具人,作用就是告诉我们,这个类调用完了,已经创建完对象了。
语法:~类名(){}
示例如下:
~Student()
{
//析构代码,可以将堆区开辟的数据做释放操作
cout << "析构函数的调用。" << endl;
}
结果如下:
最后说一下最特殊的这个–拷贝构造函数,也就是调用构造函数时需要传入类的这个构造函数,它的用处还是很多的:
- 使用已创建完毕的对象创建一个新对象(最常用)
- 值传递的方式给函数参数传值
- 以值方式返回局部变量
构造函数调用规则:拷贝>有参>无参,大于号代表优先程度,如果有“大”的系统会优先考虑调用“大”的。
4.3 初始化列表
作用:C++提供了初始化列表语法,用来初始化属性
语法:构造函数():属性1(值1),属性2(值2)…{};
示例如下:
public:
//构造函数
Student() :name("K.Fire"), id("123456")
{
cout << "无参构造函数的调用。" << endl;
}
4.4 C++对象模型和this指针
4.4.1 成员变量和成员函数分开存储
这一个知识点对我来说意义不大啊,他的意思是只有非静态变量才属于类的对象上,而静态成员(包括静态变量和静态方法)是共享的,它们不存在类上,成员函数也不在类上,只要知道这一点就行了。
4.4.2 this指针
本质:this指针的本质是指针常量,不能改变指向,可以改变数值
每个非静态函数只有一份实例,但多个同类对象会公用一块代码。也就是说当你通过一个类,创建了很多变量,这些变量中都存在同一个成员函数,成员函数只会开辟一块内存,他们都是存在一起的,而this指针的作用就是用来帮你区分这些对象。
this指针指向被调用的成员函数的所属对象。this指针有以下两个特点:
- this指针是隐含每一个非静态成员函数内的一种指针
- this指针不需定义,直接使用即可
this有以下两个用途:
- 当形参和成员变量同名时,可用到this指针区分
- 在类的非静态成员函数中返回对象本身,可以用return *this
示例如下:
Person::Person(int age)
{
age = age;
build = new Building;
}
如果按这样去写这段代码的话,编译器就傻了,他不知道这里的age哪个是形参哪个是成员变量,所以出现了下面的结果。
结果如下:
这显然不是我们想要的结果,但当我们加上this指针就可以解决这个问题了。
示例如下:
Person::Person(int age)
{
this->age = age;
build = new Building;
}
结果如下:
其实我们没必要用this指针去做这件事情,我们在创建这个类的时候尽量把类的成员变量和形参变量的命名区分开就好了。
至于这第二个用法,我就更用不到了,第二个用途有一个重要的概念叫“链式调用”,可能是用来让代码可读性更强的,但是我用不太到,我就懒得写了。
4.4.3 const修饰成员函数–常函数
语法:返回类型 函数名() const {}
常函数内不可修改成员属性,也就是说到现在为止我的理解内常函数只能用来打印输出一些东西。如果硬要在常函数里修改一些成员变量,只需要在声明前加上关键字mutable
4.5 友元
在程序中,有些私有属性,想让类外特殊的一些函数或类访问私有权限内的东西,这就需要用到友元,它的关键字也很形象:friend
4.5.1 全局函数做友元
语法:friend 返回值类型 函数名(){};
注意这里的语法是将函数声明前加上friend关键字写在类内。
示例如下:
class Building
{
//goodGay全局函数是Building的好朋友,可以访问private权限的内容
friend void goodGay(Building* build);
public:
Building()
{
m_Sitting = "客厅";
m_room = "卧室";
}
public:
string m_Sitting;
private:
string m_room;
};
//全局函数做友元
void goodGay(Building* build)
{
cout << "正在访问:" <<build->m_room<< endl;
}
这样我们调用goodGay函数就可以访问Building类中m_roon这个私有权限的成员属性了
结果如下:
4.5.2 类做友元
同样,只需在类内加上friend关键字的另一个类,即可在该类内创造成员函数去访问private权限的内容
示例如下:
class Building;
//设计一个类
class Person
{
//类做友元
public:
Person(int age);
Building* build;
void gg();
int age;
};
class Building
{
//Person类是它的好朋友,可以访问private权限内容
friend class Person;
public:
Building()
{
m_Sitting = "客厅";
m_room = "卧室";
}
public:
string m_Sitting;
private:
string m_room;
};
//类外写成员函数
void Person::gg()
{
cout << "正在访问:" << build->m_room << endl;
}
结果如下:
4.5.3 成员函数做友元
成员函数做友元 其实和上面两个类似,就只有一点需要注意一下:在类内定义友元时,friend后需要用“类名::成员函数”的方式去声明友元
它这一点正好和在类外定义友元的方式相似,在ROS中常用到这种定义方式,所以我就讲一下这个。
语法:返回值类型 类名::函数名
示例如下:
//类外写成员函数
void Person::gg()
{
cout << "正在访问:" << build->m_room << endl;
}
Person::Person(int age)
{
this->age = age;
build = new Building;
}
类外写成员函数的步骤:
- 先创建一个类,在类内对该成员函数进行声明
- 在类外,用“返回值类型 类名::成员函数名(参数){}”的方式去书写成员函数体
4.6 运算符重载
运算符重载就是对已有的运算符的一个重新定义,使用关键字“operator+”,运算符重载可以赋予其另一种功能,以适应不同的数据类型,这样说属实有点抽象…
4.6.1 加号运算符重载
加号运算符重载有两种方式:成员函数重载/全局函数重载,在这里我只用成员函数重载为例,说明一下加号运算符重载,正好顺便联系一下类外编写成员函数。
语法:返回值类型 operator+(参数){}
我们知道正常情况下,下面这种加法运算肯定是报错的,因为编译器不知道两个人相加会得到什么,难道是人类幼崽??
Person d = a + b;
但是如果我们对加号进行运算符重载,编译器就可以帮我们实现两个人相加,但当然不是创造人类幼崽!
示例如下:
class Person
{
public:
int a;
int b;
Person operator+(Person& p);
};
//类外定义成员函数
Person Person::operator+(Person& p)
{
Person temp;
temp.a = this->a + p.a;
temp.b = this->b + p.b;
return temp;
}
int main() {
Person a;
a.a = 10;
a.b = 10;
Person b;
b.a = 10;
b.b = 10;
Person c = a.operator+(b);//原版
Person d = a + b;//简化版
cout << "c:" << c.a << endl;
cout << "d:" << d.b << endl;
system("pause");
return 0;
}
结果如下:
可以看到我们对加法运算符进行重载后,编译器就能给我们实现两个人相加了,而且代码中我写了两种使用方法,第一种“原版”其实就是调用了a这个对象的一个成员函数的方式;第二种“简化版”就更好了,它看起来和我们正常的加法没什么不同!
这种运算符重载看起来挺有趣,但也有两点需要注意一下:
- 对于内置的数据类型的表达式运算符是不可以改编的
也就是说对于原来的整型、浮点型的相加减乘除都不可以改变,原来一加一等于二,你不能给它胡改,改成一加一等于三,那肯定不行。
- 不要滥用运算符重载
一个原因是C++为我们提供的运算已经很多了,一般来说已经够用了,对于这种两个类或者结构体相加的运算,你就多写两行代码嘛,也没什么,对于ROS中功能包的编写,这些运算符应该是够用了。
另一个原因是,运算符重载多了,自己也给载傻了,别写着写着把自己写懵逼了…
4.6.2 左移运算符重载
作用:可以输出自定义的数据类型
也就是说,如果我们定义了一个人的类,通过这个类创建了一个对象,而你想通过cout打印这个对象,直接打印肯定是不行的,这就需要将左移运算符重载。
示例如下:
class Person
{
public:
int m_a;
int m_b;
};
//只能利用全局函数重载左移运算符
ostream & operator<<(ostream& cout, Person p)
{
cout << "m_a= " << p.m_a << " m_b= " << p.m_b << endl;
return cout;
}
void test01()
{
Person p;
p.m_a = 10;
p.m_b = 10;
cout << p << endl;
}
int main() {
test01();
system("pause");
return 0;
}
结果如下:
可以看到这样就正确输出了人这个对象的两个成员属性
但其实我觉得这部分有点鸡肋,如果想把C++学精通,这肯定是必不可少的,但是对于我来说我的目标还是ROS,对于C++的专业知识肯定是多多益善,但是我觉得还是需要侧重于常用的语法知识。
说这么多,其实就是我不想继续写这部分了,散会!
4.7 继承
有一些类除了拥有上一级的性质,还有自己的特性,这时我们就可以考虑使用继承的技术,这将极大的减少重复代码!
语法:class 子类 :继承方式 父类
示例如下:
//继承
class base
{
public:
void header()
{
cout << "*首页* *登录* *注册* " << endl;
}
void footer()
{
cout << "*帮助* *中心* *微信* *联系方式*" << endl;
}
void left()
{
cout << "----------公共分类列表----------" << endl;
}
protected:
int code;
};
class Python : private base
{
public:
void content()
{
cout << "Python页面视频" << endl;
}
};
base这个类是Python、Java、C++这些界面共有的一些内容,我们可以把他们定义为一个父类,让接下来的Python等特殊的类去继承他们,并且他们可以有自己特有的成员属性。
但是不同的继承方式,继承出来的子类又有一些区别,继承方式有三种:公共继承、保护继承、私有继承,他们和我们在写类的时候的访问权限是相同的,但不论哪种继承方式,子类都无法访问父类中私有权限的内容。
他们的区别如下图所示:
下面通过实验验证一下
//继承
class base
{
public:
void header()
{
cout << "*首页* *登录* *注册* " << endl;
}
void footer()
{
cout << "*帮助* *中心* *微信* *联系方式*" << endl;
}
void left()
{
cout << "----------公共分类列表----------" << endl;
}
protected:
int code;
};
class Java : public base
{
friend void test01();
public:
Java(int code)
{
this->code = code;
}
public:
void content()
{
cout << "Java页面视频" << endl;
}
};
class Python : private base
{
public:
void content()
{
cout << "Python页面视频" << endl;
}
};
void test01()
{
Java xx(10);
xx.header();
xx.left();
xx.footer();
xx.content();
cout << xx.code << endl;
}
int main() {
//test01();
Python x(10);
x.header();
x.left();
x.footer();
x.content();
system("pause");
return 0;
}
结果如下:
因为Python在继承时使用的是私有继承,父类中public的内容也都变成了private的权限,因此无法在类外访问这些成员函数。
下面,我们再通过一个测试函数创建一个Java的子类,而它的公共权限为保护继承,我们看一下效果。
//继承
class base
{
public:
void header()
{
cout << "*首页* *登录* *注册* " << endl;
}
void footer()
{
cout << "*帮助* *中心* *微信* *联系方式*" << endl;
}
void left()
{
cout << "----------公共分类列表----------" << endl;
}
protected:
int code;
};
class Java : public base
{
friend void test01();
public:
Java(int code)
{
this->code = code;
}
public:
void content()
{
cout << "Java页面视频" << endl;
}
};
class Python : private base
{
public:
void content()
{
cout << "Python页面视频" << endl;
}
};
void test01()
{
Java xx(10);
xx.header();
xx.left();
xx.footer();
xx.content();
cout << xx.code << endl;
}
int main() {
test01();
system("pause");
return 0;
}
结果如下:
本来Java其实也是访问不了父类的public的内容的,因为在继承的时候我们使用的是保护继承,但是我在Java这个类中将全局函数设置为了友元,他们变成了一对好基友,这就完全没问题了!
子类和父类的关系与拷贝构造函数有所不同,拷贝构造函数构造函数和析构函数执行的顺序是:被拷贝构造函数->拷贝构造函数->被拷贝析构函数->拷贝析构函数,相当于是拷贝类完全跟着被拷贝的类的动作去做。
而这里的继承的执行顺序是:父类构造函数->子类构造函数->子类析构函数->父类析构函数,相当于父类在子类的外层,父母是儿女的保护伞,遇到困难父母先站出来为我们遮风挡雨,困难被客服后也是让子女先撤退,自己殿后。
//继承
class base
{
public:
base()
{
cout << "base构造函数" << endl;
}
~base()
{
cout << "base析构函数" << endl;
}
};
class Java : public base
{
friend void test01();
public:
Java()
{
cout << "子类构造函数" << endl;
}
~Java()
{
cout << "子类析构函数" << endl;
}
};
class Python : private base
{
public:
void content()
{
cout << "Python页面视频" << endl;
}
};
void test01()
{
Java xx;
}
int main() {
test01();
system("pause");
return 0;
}
结果如下:
在继承后,如果子类和父类出现同名的成员属性和成员函数,在访问子类中的成员函数和成员属性时可以直接访问,但调用父类中的成员属性和成员函数需要注明作用域。
示例如下:
//继承
class base
{
public:
base()
{
f_a = 10;
}
void fun()
{
cout << "base的成员函数调用" << endl;
}
int f_a;
};
class son : public base
{
public:
int f_a;
son()
{
f_a = 200;
}
void fun()
{
cout << "son的成员函数调用" << endl;
}
};
int main() {
son s;
cout << "f_a= " << s.f_a << endl;//直接打印输出,打印的是子类的
cout << "f_a= " << s.base::f_a << endl;//想要打印父类的属性,需要加一个作用域
s.fun();
s.base::fun();
system("pause");
return 0;
}
结果如下:
4.8 多态
4.8.1 基本概念
多态分为两种:静态多态和动态多态,函数重载、运算符重载都属于静态多态;派生类和虚函数都可以实现运行时多态。
- 静态多态:地址早绑定,在编译阶段确定函数地址
- 动态多态:地址晚绑定,在运行阶段确定函数地址
示例如下:
//多态
//动物类
class animal
{
public:
void speak()
{
cout << "动物在说话" << endl;
}
};
//猫类
class cat : public animal
{
void speak()
{
cout << "小猫在说话" << endl;
}
};
//地址早绑定,在编译阶段确定函数地址
void doSpeak(animal& animal)
{
animal.speak();
}
void test01()
{
cat cat;
doSpeak(cat);
}
int main() {
test01();
system("pause");
return 0;
}
结果如下:
这里输出的结果是动物在说话,但是我们想要的结果其实是想让小猫说话,这就需要用到动态多态的方法,设计一个虚函数。
示例如下:
class animal
{
public:
//虚函数
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
结果如下:
根据上面的代码,我们大致可以总结出多态的两个必备条件:
- 要有继承关系
- 子类中要重写父类函数
重写:函数返回值类型、函数名、函数形参列表完全相同
4.8.2 纯虚函数和抽象类
在多态中,父类中虚函数的实现是毫无意义的,主要是调用子类中重写的内容,因此可将父类中的虚函数改为纯虚函数
语法:virtual 返回值类型 函数名(参数列表)=0;
相应的,当这个类中有了纯虚函数后,这个类就被成为抽象类
抽象类的特点如下:
- 无法实例化对象
- 子类必须重写抽象类中的抽象函数,否则也为抽象类
示例如下:
//多态
//动物类
class animal
{
public:
//纯虚函数
virtual void speak() = 0;
};
//猫类
class cat : public animal
{
void speak()
{
cout << "小猫在说话" << endl;
}
};
//地址早绑定,在编译阶段确定函数地址
//如果想让猫说话,那么需要晚绑定
void doSpeak(animal& animal)
{
animal.speak();
}
void test01()
{
cat cat;
doSpeak(cat);
}
int main() {
test01();
system("pause");
return 0;
}
结果如下:
4.8.3 虚析构和纯虚析构
多态使用时,如果子类中的属性开辟到堆区,那么父类指针在释放时无法调用子类的析构代码,会造成内存泄漏
解决的方式就是将父类中的析构函数改为虚析构或纯虚析构
语法:virtual ~类名()= 0;
示例如下:
//多态
//动物类
class animal
{
public:
//纯虚函数
virtual void speak() = 0;
virtual ~animal()
{
cout << "animal的构造函数" << endl;
}
};
//猫类
class cat : public animal
{
public:
cat(string name)
{
m_name = new string(name);
}
void speak()
{
cout << "小猫在说话" << endl;
}
~cat()
{
if (m_name != NULL)
{
cout << "cat析构函数调用" << endl;
delete m_name;
m_name = NULL;
}
}
string *m_name;
};
void test01()
{
animal* anumal = new cat("Tom");
anumal->speak();
delete anumal;
}
int main() {
test01();
system("pause");
return 0;
}
结果如下:
如果你没有在父类中写虚析构或者纯虚析构的话,子类的析构函数是不会执行的,所对应的数据也不会释放。
实例中我去掉了父类的虚构函数:
class animal
{
public:
//纯虚函数
virtual void speak() = 0;
};
结果如下:
此外,还有一点需要注意:纯虚析构与纯虚函数有所不同,纯虚析构不能只“virtual ~类名()= 0”这一句话,纯虚析构需要有内容,因为编译器认为,父类中可能有一些开辟到堆区的数据,需要通过父类的析构函数释放,所以平时我们一般这么去写:
//猫类
class cat : public animal
{
public:
cat(string name)
{
m_name = new string(name);
}
void speak()
{
cout << "小猫在说话" << endl;
}
~cat()
{
if (m_name != NULL)
{
cout << "cat析构函数调用" << endl;
delete m_name;
m_name = NULL;
}
}
string *m_name;
};
cat::~cat()
{
cout << "animal的构造函数" << endl;
}
使用这种类外写函数的方式去书写纯虚析构的函数,最后,纯虚函数的类也为抽象类,同样无法创建实例对象。
以上就是我与C++的相知过程,我们进行了漫长而又深刻的对话,我了解了它的为人,不得不说它确实很厉害,也很有智慧。
但是对不起,我有喜欢的人了,没兴趣再去认识谁了,反正到头来都是要走的。
有些人二十岁就死了,等到八十岁才被埋葬。
“我心中的一团火不会熄灭!”