1.程序的内存模型
C++执行代码时,内存分为四个区域
- 代码区:存放二进制代码,由操作系统管理
- 全局区:存放全局变量和静态变量以及变量
- 栈区:编译器自动分配与释放,存放函数参数值,局部变量等
- 堆区:程序员分配和释放,如果不释放则由程序自动回收
1.1在程序运行之前
此时程序编译后生成了.exe文件,存在两个区域
-
代码区:寻访CPU执行的机器命令,代码区的共享性:目的是对于一个频繁执行的程序,只需要在内存里有一份代码就可以了,代码区的只读性:反之程序意外执行它
-
全局区:存放全局变量和静态变量,还包含了常量区,字符串常量,其他常量,这个区域的数据在程序结束之后由系统释放。(静态变量:在普通变量前面加上static)
1.2程序运行后
存在
-
栈区:由编译器自动释放,存放函数的参数值,局部变量等。
注意:不要返回局部变量的地址,因为会被编译器自动释放。但是数据可以被找到一次,因为编译器帮助做了保留,第二次就不能被找到了。 -
堆区:由程序员分配,如果程序员不释放,程序结束时系统自动释放。在堆区开辟内存需要关键字
new
new关键字
在堆区开辟内存,且返回开辟的地址值。用法:数据类型 *p = new 数据类型(value)
void func(){
int *p = new int(10);
return *p;
]
int *p = func();
cout << *p << endl;
output:
10
注意指针也是一个局部变量
如果要释放内存,那么就可以使用delete
关键字
int *p = new int(1);
delete p;
堆区数组操作:
int *p = new int[5]; //开辟了有5个元素的数组
delete[] p; //释放数组
2.引用
基本语法
赐予变量别名:数据类型 &别名 = 原名
在引用的时候,对原本的变量进行操作和对引用后的变量进行操作都会互相影响。
int a = 30;
int &b = a;
a = 20;
printf("a = %d , b = %d\n",a,b);
b = 10;
printf("a = %d , b = %d",a,b);
output:
a = 20 , b = 20
a = 10 , b = 10
引用传递
我们知道对于函数有地址传递和值传递,其实还有引用传递。
复习地址传递:
void swap(int *a,int *b){
int temp = *a;
*a = *b;
*b = temp;
}
swap(&a,&b);
接下来是引用传递:
void swap(int &a,int &b){
int temp = a;
a = b;
b = temp;
|
swap(a,b);
这样同样可以实现值的互换,与地址传递效果相同。
这里函数的形参是传入的两个参数,并且将其引用,引用指向的也是原本参数的地址,所以能够实现类似地址传递的效果,所以我们可以用引用来简化语句。
引用作为函数返回类型
当引用作为函数的返回类型的时候,有几个注意事项:
- 不要返回局部变量的引用。
int& func(){
int a = 1;
return a;
}
返回了局部变量a的引用值,因为局部变量存放在栈区,内存会被自动回收,在执行完函数之后,a的内存就被清楚了,当然就不能够正常使用了。
- 函数的调用可以作为左值
int& func(){
static int a = 1;
return a;
}
int& ref = func();
func() = 10;
cout << ref << endl;
output:
10
由于静态变量是存放在全局区的,只有在程序结束之后才会被回收,所以可以使用。
如果函数作为左值,由于函数返回的是a的引用,那么对其进行赋值操作,就是对a进行了修改,由于ref是对a的引用,那么ref也会被修改。
引用的本质
引用的本质:通过指针常量实现
int& ref = a;
就相当于
int* const ref = &a;
那么ref里面存的就是a的地址,如果进行
ref = 1;
的赋值操作的时候,编译器会发现ref为引用数据类型,那么就会自动执行
*ref = 1;
那么由于引用数据类型是作为指针常量,它指向的地址值是不可修改的,所以一个引用数据类型只能一直对一个变量进行引用。
常量引用
尝尝用来修饰形参来防止误操作
const int& ref = 10;
这就是一个常量引用,这句话就等效于
int temp = 10;
const int& ref = temp;
这样的常量引用得到的变量是不可修改的。
void func(const int& ref){
...
}
这时候就不能对原变量进行修改了
3.函数提高
函数形参默认值
可以给函数的形参赋予默认值
void func(int a = 1,int b = 2,int c = 3){
...
}
注意事项:
- 如果某一形参有了默认值,那么从该形参往后的形参都必须有默认值。
- 如果函数声明时给出了默认值,那么函数实现就不能有默认参数。
占位参数
目前还不知道有什么用
即函数形参只写数据类型而不写变量名
void func(int a,int){
...
}
并且占位参数还能够有默认参数
void func(int a,int = 10){
...
}
函数重载
C++可以使函数名相同,并且在不同情况下调用不同的同名的参数,由此提高复用性
函数重载满足的条件:
- 在同一个作用域下
- 函数名称相同
- 函数参数类型不同,或者个数不同,或者顺序不同
注意:函数的返回值不能够作为函数重载的条件
void func(int a){
cout << 1 << endl;
}
void func(){
cout << 2 << endl;
}
func();
func(1);
output:
2
1
只要有区别就能够区分出执行的函数,比如传入参数的数据类型的不同,传入参数的数量等等。
但是注意,函数的返回值不能够作为函数重载的条件。
引用作为函数重载的条件:
void func(int &a){
...
}
void func(const int &a){
...
}
int a = 1;
func(a); //调用了第一个
func(1); //调用了第二个
函数重载遇到默认值
void func(int a,int b = 10){
...
}
void func(int a){
...
}
在这种情况下会出错,编译器无法判别调用的是哪一个函数。
4.类和对象
C++面向对象的三大特性为:封装,继承,多态。
相同类型的对象,就可以称为一个类。
4.1封装
意义
- 将属性和行为作为一个整体,表现所有事物。
- 将属性和行为加入权限控制
语法
定义类
class 类名{
访问权限:
属性...
行为...
...
};
调用类
类名 变量名; //创建一个类的变量
变量名.属性 //调用变量的属性进行赋值等操作
变量名.行为 //调用变量的行为
类里面的属性还有行为都叫做成员,属性就是成员属性,行为就是成员行为。
访问权限
访问权限有三种,分别是private
,protected
,public
。
public
的成员类内可以访问,类外不可以访问。
protected
类内可以访问,类外不可以访问,并且子类可以访问父类的保护成员
private
类内可以访问,类外不可以访问,但是子类不能够访问父类的私有成员
class Node{
public:
int a;
private:
int b;
protected:
int c;
};
类外不可以访问的意思是在创建对象之后,我们不能够调用私有或者保护权限中的任何成员。
运用:有时我们需要设计一些仅使用权限关键字无法实现的功能,比如我们要实现只读或者只写功能的时候,仅依靠C++提供的三个权限是无法涵盖的,那么这时候就需要我们设计一些特定权限的成员方法来实现。
例如我们现在要实现只读和只写功能。
class Node{
public:
void printN1(){
cout << n1 <<endl;
}
void rewriteN2(int input){
n2 = input;
}
private:
int n1 = 2; //只读
int n2; //只写
};
如果所有的成员变量都是public的时候,我们可以如下快速设置成员变量:
Node n = {1,2,3...}
如果存在private或者protected就不可以使用了
struct和class的区别
都可以用来写类,但是二者默认的访问的权限不同
struct默认为公有,class默认为私有。
运用
类可以嵌套类,类可以被封装分散到其他文件。
分散过程
- 准备一个写好的类
- 首先创建一个头文件(.h文件),创建一个源文件(.cpp文件),二者命名应该相同。
- 在头文件中这样写:
#pragma once
#include<iostream>
...(所需头文件)
using namespace std;
接下来删掉所有的成员函数的实现,然后留下成员函数的声明,其余位置不动
即留下来成员函数和成员变量的声明
- 在源文件中这样写:
#include "头文件的名字.h"
仅留下函数的实现,将其余的代码全部删掉(包括class等代码)
并且这里要注意在函数的名字前面表明作用域,如下。
void class_name::func(...){
...
}
这里class_name就是我们分装的类的名称,双冒号就代表了指定作用域。
并且如果我们在封装一个类的时候用到了另一个类,或者我们在主要源文件中调用类的时候,就把那个类的头文件包含进来就可以了。
4.2对象的初始化和清理
4.2.1构造函数和析构函数
对象需要初始化和清理,如果用完之后没有进行初始化或者对象不存在初始状态,那么都会导致不可预料的后果。
构造函数和析构函数可以解决上述的问题,这两个函数编译器会自动的调用,但是如果我们不提供构造函数和析构函数,编译器提供的构造函数和析构函数就会是空实现(即啥也没有)。
- 构造函数:用于创建对象时为对象的成员属性赋值。
- 析构函数:用于在对象销毁之前进行清理。
构造函数和析构函数都是编译器自动调用,即使我们写出了这两个函数也无需自己调用,因为编译器会自动调用我们所实现的函数。
构造函数语法:
类名(){...}
即:
- 不需要写返回值类型或者返回值
- 函数名称与类名相同
- 构造函数可以写入参数,可以进行函数重载
- 调用对象时编译器自动调用,并且只会调用一次
析构函数的语法:
~类名(){}
- 不写返回值类型和返回值
- 名称与类名相同,并且在名称前面需要加上
~
- 析构函数不能够写入参数,不可以进行函数重载
- 对象销毁前编译器自动调用,并且只会调用一次
注意这里构造函数和析构函数都是写到类定义的内部,并且也需要指定作用域。
4.2.2构造函数的分类和调用
按照参数分类分为:有参构造和无参构造
按照类型分为:普通构造和拷贝构造
拷贝构造函数的写法:
类名(const 类名 &变量名){
...
}
拷贝构造函数可以将传入的类的所有属性拷贝到构建的类的身上。
创建方法:
括号法
括号法调用有参构造函数类名 变量名(传入参数)
括号法调用拷贝构造函数类名 变量名(传入类参数)
注意调用类的默认构造函数的时候,不要加括号。
如果你加一个括号并且不传参数,编译器会将其认为为一个函数的声明
显示法
显示法调用有参构造
类名 变量名 = 类名(参数)
显示法调用拷贝构造
类名 变量名 = 类名(类参数)
匿名对象:类名(参数)
,当前执行之后,系统会自动回收掉匿名对象。
注意不要用拷贝构造函数来初始化匿名对象,编译器将其认为为声明。
隐式转换法
隐式转换法调用有参构造
类名 变量名 = 参数
隐式转换法调用拷贝构造
类名 变量名 = 类参数
4.2.3拷贝构造函数调用时机
在三个情况下进行调用
- 当使用一个创建完毕的对象来创建一个新对象
- 使用值传递的方式给函数传参
- 以值方式返回局部对象
使用值传递的方式给函数传参时,会拷贝一个临时的副本然后进行传参。
当以值方式返回局部对象时,返回的并不是创建出来的局部对象,而是根据局部对象拷贝了一份然后进行了返回。
4.2.4构造函数调用规则
默认情况下编译器至少给一个类添加3个函数
- 默认构造函数(无参数,函数体为空)
- 默认析构函数(无参数,函数体为空)
- 默认拷贝构造函数。
构造函数的调用规则:
- 如果用户定义有参构造函数,C++不再提供默认无参构造,但是提供默认拷贝构造。
- 如果用户定义拷贝构造函数,C++不再提供其他构造函数。
在编译器不提供的情况下需要调用的话就会导致错误。
4.2.5深拷贝和浅拷贝
- 浅拷贝:简单的拷贝赋值
- 深拷贝:在堆区重新申请空间,进行拷贝操作
参考如下代码:
#include<iostream>
using namespace std;
class Person{
public:
Person(){
cout << "无参构造函数调用" << endl;
}
Person(int age,int height){
m_age = age;
m_height = new int(height);
cout << "有参构造函数调用" << endl;
}
~Person(){
//释放内存
if(m_height != NULL){
delete m_height;
m_height = NULL;
}
cout << "析构函数调用" << endl;
}
public:
int m_age;
int *m_height;
};
int main(){
Person p1(18,180);
cout << "p1的年龄为:" << p1.m_age << " p1的身高为:" << *p1.m_height << endl;
Person p2(p1);
return 0;
}
在这里我们想要通过在堆区开辟新的内存存储对象成员,但是这种情况会出现以下问题:
在这里,如果使用拷贝构造操作复制了一个使用了深拷贝的类对象,那么他所拥有的深拷贝的变量指向的地址是与原始对象相同的,如果在析构函数中将其释放掉之后,这时候原始对象的指针指向的内存仍然没有变化,但是那段内存已经被释放了,那么就会导致原始对象进行释放的时候变为了一个非法操作。
这就是浅拷贝的问题,也就是会导致内存重复释放。
那么解决该问题就需要用到深拷贝。
深拷贝是通过在拷贝对象的时候从新开辟一段内存,而不去利用上一个对象指向的内存。注意这里浅拷贝运用的拷贝构造函数是编译器提供的,所以才会导致错误的出现,那么我们只需要自己实现一下能够重新开辟内存的拷贝构造函数就可以了。
编译器提供的拷贝构造函数如下:
Person(const Person &p){
m_age = p.m_age;
m_height = p.m_height;
}
那么我们只需要将其改为:
Person(const Person &p){
m_age = p.m_age;
//实现深拷贝
m_height = new int(*p.m_height)
}
注意:在构造函数中开辟了堆区内存之后,需要在析构函数中进行内存释放。
4.2.6初始化列表
对类中的成员属性进行方便的初始化操作
语法:构造函数():属性1(值1),属性2(值2),...{}
#include<iostream>
using namespace std;
class Person{
public:
Person():m_a(1),m_b(2),m_c(3){
cout << "无参构造函数调用" << endl;
}
Person(int a,int b,int c):m_a(a),m_b(b),m_c(c){
cout << "有参构造函数调用" << endl;
}
~Person(){
cout << "析构函数调用" << endl;
}
public:
int m_a;
int m_b;
int m_c;
};
int main(){
Person p1;
cout << p1.m_a << ' ' << p1.m_b << ' ' << p1.m_c << endl;;
Person p2(3,4,5);
cout << p2.m_a << ' ' << p2.m_b << ' ' << p2.m_c << endl;;
return 0;
}son p1(3,4,5);
cout << p1.m_a << ' ' << p1.m_b << ' ' << p1.m_c << endl;;
return 0;
}
output:
无参构造函数调用
1 2 3
有参构造函数调用
3 4 5
析构函数调用
析构函数调用
4.2.7类对象作为类成员
类中的成员可以是别的类的对象(套娃)。
注意废除先后关系是外部先废除然后内部再废除,内部先构造外部再构造。
4.2.8静态成员
static
的成员变量和函数
- 对于静态成员变量
所有对象共享同一份数据,在编译阶段分配内存,类内声明,类外初始化。
类内声明类外初始化:
class C{
public:
static int m_A;
};
int C::m_A = 1;
共享同一份数据:
class C{
public:
static int m_A;
};
int C::m_A = 1;
void test(){
C c1;
cout << c1.m_A << endl;
C c2;
c2.m_A = 2;
cout << c1.m_A << endl;
}
output:
1
2
静态成员变量可以通过类名进行访问:
class C{
public:
static int m_A;
};
int C::m_A = 1;
void test(){
cout << C::m_A <<endl;
}
但是也需要访问权限为public
- 对于静态成员函数
所有对象共享同一个函数,静态成员函数只能够访问静态成员变量。
访问方式:
class C{
public:
static void f(){
cout << '1' << endl;
}
};
void test(){
//1.
C c1;
c1.f();
//2.
C::f();
}
并且静态成员函数只能访问静态成员变量。
4.3C++对象模型和this指针
4.3.1成员变量和成员函数分开存储
在C++里,类内的成员变量和成员函数分开存储。
-
空对象占用内存为1字节:C++编译器会给每个空对象也分配一个字节空间,是为了区分空对象占用内存的位置
-
存储了一个非静态变量的对象:占用了4字节。
-
静态成员变量在类中不占用类的内存:不属于类的对象上。
-
静态成员变量和非静态成员变量都不占用类的内存:都不属于类的对象上。
4.3.2this指针
this指针指向被调用的成员函数所属的对象。
this指针隐含在每一个非静态成员函数内,不需要定义,可以直接使用。
用途:
- 形参和成员变量重名的时候,用this指针来区分。
当然也可以自己在定义类中的成员的时候进行一些命名的变动,这里主要解释使用this指针。
class C{
public:
C(int A){
this->A = A;
}
public:
int A;
};
void test(){
C c1(1);
cout << c1.A << endl;
}
谁在调用这个函数,this指针就会指向谁,在这里调用c1的有参构造函数,那么就指向了c1
的A
。
- 类的非静态成员函数中返回对象本身,可以使用 return *this
this是指向对象的一个指针,那么*this就代表了对象自己。
那么我们就能够实现这样的操作。
#include<iostream>
using namespace std;
class C{
public:
C(int A){
this->A = A;
}
C& add(C &c){
this->A += c.A;
return *this;
}
public:
int A;
};
void test(){
C c1(1);
C c2(2);
c2.add(c1).add(c1).add(c1).add(c1);
cout << c2.A << endl;
}
output:
6
注意返回引用值,因为根据之前学过的拷贝构造函数调用的原理,这里如果返回的是不加引用符号的值,那么传递回去的就不是对象的本体,而是本体的复制品,然后每次调用都会出现复制品,对于最后来说我们就只是调用了一次add
函数。
4.3.3空指针访问成员函数
C++中空指针可以调用成员函数,但是如果函数中存在this指针,就需要加以修改。
class Person{
public:
void showClassName(){
cout << "Person" << endl;
}
void showAge(){
cout << m_age << endl;
}
public:
int m_age;
};
void test(){
Person* p = NULL;
p->showClassName();
}
在这种情况下运行是没有问题的。
class Person{
public:
void showClassName(){
cout << "Person" << endl;
}
void showAge(){
cout << m_age << endl;
}
public:
int m_age;
};
void test(){
Person* p = NULL;
p->showAge();
}
但是在这种情况下就会出现段错误。
这是因为我们在类内函数中调用成员变量的时候,成员变量的前面都默认被加上了this->
,也就是说现在属于是函数体内存在this指针的情况。
注意我们创建对象时创建了一个空指针,也就是说我们压根没有创建出来任何对象,那么this指向的就是一个空,那么this就没有办法指向一个成员变量。
我们只需要进行判断在this指针为空的时候直接return
掉就可以了。
class Person{
public:
void showClassName(){
cout << "Person" << endl;
}
void showAge(){
if(this == NULL)return;//加入这一行
cout << m_age << endl;
}
public:
int m_age;
};
void test(){
Person* p = NULL;
p->showAge();
}
4.3.4const修饰成员函数
常函数:
- 成员函数后面加上const后就成为常函数
- 常函数内不可以修改成员属性
- 成员属性声明的时候加上
mutable
关键字之后,就能够在常函数中进行修改
这里注意this指针的本质其实是指针常量,是不能够修改指向的目标的,也就是说本来this只能指向当前的类,但是this指向的类中的成员变量是可以修改的。
如果我们在给它加一个const修饰,就变成了this指向的值也不可以改变了,这个const就加在了成员函数的后面。
class Person{
public:
Person(int age){
m_age = age;
}
void changeAge()const{ //常函数
this->m_age = 1;
}
public:
int m_age;
};
void test(){
Person p1(3);
p1.changeAge();
}
这时候在常函数内进行成员变量的修改显然会报错。
但是加上mutable
就可以了:
class Person{
public:
Person(int age){
m_age = age;
}
void changeAge()const{
this->m_age = 1;
}
public:
mutable int m_age; //加上mutable关键字,使得能够在常函数中被修改
};
void test(){
Person p1(3);
p1.changeAge();
}
常对象:
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数
创建常对象就是在创建对象的操作前加上const修饰。
在常对象中,普通的成员变量无法被直接修改,但是加上了mutable
关键字的变量可以被修改。
class Person{
public:
Person(int age,int height){
m_age = age;
m_height = height;
}
void changeAge()const{
this->m_age = 1;
}
public:
mutable int m_age;
int m_height;
};
void test(){
const Person p1(3,180);
p1.m_age = 1;
p1.m_height = 100; //这一行会报错
}
并且普通的函数也不能够调用,只能够调用常函数。
4.4友元
使用友元friend
,使得能够让类外的某些函数等对类内的私有权限的成员进行访问。
友元有三种实现方式:
- 全局函数作为友元
- 类作为友元
- 成员函数作为友元
4.4.1全局函数作为友元
首先,我们直接访问公有权限的成员变量是没有问题的。
class A{
public:
A(){
m_a = 1;
m_b = 2;
}
public:
int m_a;
private:
int m_b;
};
void visit(A &a){
cout << a.m_a << endl;
}
void test(){
A a1;
visit(a1);
}
正常情况下如下情况就会出问题。
class A{
public:
A(){
m_a = 1;
m_b = 2;
}
public:
int m_a;
private:
int m_b;
};
void visit(A &a){
cout << a.m_a << endl;
cout << a.m_b << endl;
}
void test(){
A a1;
visit(a1);
}
但是我们可以运用友元。
将函数名复制,然后再类内加上friend 函数声明
,这样就声明了此函数为该类的友元。
如下,就可以正常访问了。
class A{
friend void visit(A &a);
public:
A(){
m_a = 1;
m_b = 2;
}
public:
int m_a;
private:
int m_b;
};
void visit(A &a){
cout << a.m_a << endl;
cout << a.m_b << endl;
}
void test(){
A a1;
visit(a1);
}
4.4.2类作为友元
首先看一下正常访问公有权限:
class Building{
public:
Building(); //注意声明
public:
string m_sittingRoom;
private:
string m_bedRoom;
};
class Person{
public:
Person(); //注意声明
void visit();
public:
Building *b1; //在Person类内创建Building对象
};
//Building类的构造函数
Building::Building(){
m_sittingRoom = "BoynextDoor";
m_bedRoom = "DoULikeWhatyouSee";
}
//Person类的构造函数
Person::Person(){
b1 = new Building; //构造在堆区
}
void Person::visit(){
cout << "visiting" << b1->m_sittingRoom << endl;
//cout << "visiting" << b1->m_bedRoom << endl;
}
void test(){
Person p1;
p1.visit();
}
可以发现是没有问题的。
注意这里使用了在全局进行类内成员函数实现的操作,具体做法是:
- 在类内进行函数声明。
- 在类外首先写上同样的函数名称,然后在前面加上
类名::
来声明具体作用域。
同样,我们这时候直接访问私有的成员变量是会出错的。
class Building{
public:
Building();
public:
string m_sittingRoom;
private:
string m_bedRoom;
};
class Person{
public:
Person();
void visit();
public:
Building *b1;
};
Building::Building(){
m_sittingRoom = "BoynextDoor";
m_bedRoom = "DoULikeWhatyouSee";
}
Person::Person(){
b1 = new Building;
}
void Person::visit(){
cout << "visiting" << b1->m_sittingRoom << endl;
cout << "visiting" << b1->m_bedRoom << endl;
}
void test(){
Person p1;
p1.visit();
}
这时候就需要在被访问的类内进行友元操作,将访问它的类当成友元
在这里就是将Person
类当作Building
的友元
这样就能够正常运行了。
class Building{
friend class Person; //友元
public:
Building();
public:
string m_sittingRoom;
private:
string m_bedRoom;
};
class Person{
public:
Person();
void visit();
public:
Building *b1;
};
Building::Building(){
m_sittingRoom = "BoynextDoor";
m_bedRoom = "DoULikeWhatyouSee";
}
Person::Person(){
b1 = new Building;
}
void Person::visit(){
cout << "visiting" << b1->m_sittingRoom << endl;
cout << "visiting" << b1->m_bedRoom << endl;
}
void test(){
Person p1;
p1.visit();
}
4.4.3成员函数作为友元
注意,如果前面的类用到了后边的类,那么前面的类之前要进行后面的类的声明,不然编译器会找不到。
class Building;
class Person{
public:
Person();
void visit();
void visit2();
public:
Building *building;
};
class Building{
friend void Person::visit(); //注意这里也要说明作用域,不然编译器找不到
public:
Building();
public:
string m_sittingRoom;
private:
string m_bedRoom;
};
Person::Person(){
building = new Building;
}
void Person::visit(){
cout << building->m_bedRoom << endl;
}
void Person::visit2(){
cout << building->m_bedRoom << endl; //此处会报错
}
Building::Building(){
m_sittingRoom = "BoyNextDoor";
m_bedRoom = "DoYouLikeWhatYouSee";
}
void test(){
Person p1;
p1.visit();
p1.visit2();
}
这样就实现了visit函数可以访问私有属性,而visit2不能够访问私有属性。
4.5运算符重载
4.5.1加号运算符重载
此方法主要用于自定义加法。
比如我们在自定义的类之间要进行加法操作,这时候我们就需要自己写一段函数来实现加法,但是编译器给我们提供了一段模板来实现这个函数,并且通过这个模板我们可以简化掉实现加法操作时候对函数的调用。
对于运算符的重载,可以通过成员函数或者是全局函数。
通过成员函数进行加号运算符重载
class C{
public:
C(int a,int b){
m_A = a;
m_B = b;
}
//在这里
C operator+(C &c){
C temp(m_A + c.m_A,m_B + c.m_B);
return temp;
}
public:
int m_A;
int m_B;
};
void test(){
C c1(1,1);
C c2(2,2);
C c3 = c1 + c2;
cout << c3.m_A << ' ' << c3.m_B << endl;
}
output:
3 3
通过全局函数重载加号运算符
class C{
public:
C(int a,int b){
m_A = a;
m_B = b;
}
public:
int m_A;
int m_B;
};
//在这里
C operator+(C &c1,C &c2){
C temp(c1.m_A + c2.m_A,c1.m_B+c2.m_B);
return temp;
}
void test(){
C c1(1,1);
C c2(2,2);
C c3 = c1 + c2;
cout << c3.m_A << ' ' << c3.m_B << endl;
}
output:
3 3
注意这里直接使用加号是被简化过的,本质是:
C c3 = c1.operator+(c2)
C c3 = operator+(c1,c2)
并且运算符重载函数也可以发生函数重载
class C
{
public:
C(int a, int b)
{
m_A = a;
m_B = b;
}
C operator+(C &c1)
{
C temp(m_A + c1.m_A, m_B + c1.m_B);
return temp;
}
C operator+ (int x)
{
C temp(m_A + x, m_B + x);
return temp;
}
public:
int m_A;
int m_B;
};
void test()
{
C c1(1, 1);
C c2(2, 2);
C c3 = c1 + c2;
cout << c3.m_A << ' ' << c3.m_B << endl;
c3 = c3 + 10;
cout << c3.m_A << ' ' << c3.m_B << endl;
}
output:
3 3
13 13
注意,编译器内置的数据类型的表达式的运算符是不能改变的。
4.5.2左移运算符重载
如果我们想要实现通过cout << 类名
来直接输出信息,那么就需要重载左移运算符。
如果想实现类名在cout后面,那么通过成员函数来写是不可能实现的,所以我们要通过全局函数。
注意这里将cout也传入了函数,cout
是输出流对象,即ostream
class C
{
public:
C(int a, int b)
{
m_A = a;
m_B = b;
}
public:
int m_A;
int m_B;
};
//在这里
ostream operator<< (ostream &cout,C &c){
cout << c.m_A << ' ' << c.m_B;
return cout;
}
void test()
{
C c1(1, 1);
cout << c1 << endl;
}
这里想要cout
能够进行链式编程,那么就要在返回对象的时候继续返回ostream
类型,即返回cout
。
并且如果想要输出私有成员,在类内声明函数为友元就可以了。
4.5.3递增运算符重载
道理同上,这里给出案例。
class MyInteger{
friend ostream& operator<< (ostream &cout,MyInteger &myint);
public:
MyInteger(){
m_NUM = 0;
}
//前置递增
MyInteger& operator++(){
m_NUM++;
return *this;
}
//后置递增
//后置递增要返回值,因为临时变量会被释放
MyInteger operator++(int){
MyInteger temp = *this;
m_NUM++;
return temp;
}
private:
int m_NUM;
};
//实现输出操作
ostream& operator<< (ostream &cout,MyInteger &myint){
cout << myint.m_NUM;
return cout;
}
4.5.4赋值运算符重载
其实C++编译器至少给一个类添加四个函数,分别是默认的构造函数,析构函数,拷贝构造函数和赋值运算符。
实现:
class Person{
public:
Person(int age){
m_Age = new int(age);
}
~Person(){
if(m_Age != NULL){
delete m_Age;
m_Age = NULL;
}
}
Person& operator=(Person &p){
//先判断属性是否还在堆区,先释放干净再进行深拷贝
if(m_Age != NULL){
delete m_Age;
m_Age = NULL;
}
m_Age = new int(*p.m_Age);
return *this;
}
int *m_Age;
};
void test(){
Person p1(18);
Person p2(19);
Person p3(20);
p3 = p2 = p1;
cout << *p1.m_Age << ' ' << *p2.m_Age << ' ' << *p3.m_Age << endl;
}
4.5.5关系运算符重载
实现自定义类型比较
class Person{
public:
Person(int age,string name){
m_age = age;
m_name = name;
}
bool operator==(Person &p){
if(m_name == p.m_name && m_age == p.m_age){
return 1;
}
return 0;
}
bool operator!=(Person &p){
if(m_name != p.m_name || m_age != p.m_age){
return 1;
}
return 0;
}
bool operator>(Person &p){
if(m_age > p.m_age){
return 1;
}else return 0;
}
bool operator<(Person &p){
if(m_age > p.m_age){
return 0;
}else return 1;
}
bool operator<=(Person &p){
if(m_age > p.m_age){
return 0;
}else return 1;
}
bool operator>=(Person &p){
if(m_age >= p.m_age){
return 1;
}else return 0;
}
int m_age;
string m_name;
};
void test(){
Person p1(18,"Van");
Person p2(18,"Van");
cout << (p1 == p2) << endl;
cout << (p1 != p2) << endl;
Person p3(20,"OK");
Person p4(99,"SHI");
cout << (p1 > p2) << endl;
cout << (p1 < p2) << endl;
cout << (p1 >= p2) << endl;
cout << (p1 <= p2) << endl;
}
output:
1
0
0
1
1
1
4.5.6函数调用运算符重载
这里指的是()
这个符号可以进行重载,在重载时候运行的方式与函数十分类似故称作仿函数。
class Print{
public:
void operator()(string x){
cout << x << endl;
}
};
void test(){
Print print1;
print1("SOSOSOSOSO\n");
Print()("SOSOSOSOSO\n"); //匿名对象写法
}
可以看成与正常函数十分相似。
4.6继承
4.6.1基础语法
在上下级之间有相同点的时候,我们可以利用继承来缩短工作量和重复代码。
格式:class 子类: 继承方式 父类
子类也可以称为派生类,父类可以成为基类。
比如说猫里面有英国短毛猫,但是所有的猫都是四条腿两个耳朵等。
class Cat{
public:
void init(){
m_foot_num = 4;
m_ear_num = 2;
m_eye_num = 2;
m_is_furry = 1;
}
public:
int m_foot_num;
int m_ear_num;
int m_eye_num;
bool m_is_furry;
};
//继承格式
//这样可以将大类里面的代码全部继承过来
class Felinae:public Cat{
public:
void init(){
name = "Felinae";
}
public:
string name;
};
使用继承我们可以省去很多代码。
4.6.2继承方式
也是有公共继承,保护继承,私有继承三类。
父类 | 继承方式 | 子类 |
---|---|---|
私有权限 | 公共继承 | 不能访问 |
公共权限 | 公共权限 | |
保护权限 | 保护访问 | |
私有权限 | 私有继承 | 不能访问 |
公共权限 | 私有权限 | |
保护权限 | 私有权限 | |
私有权限 | 保护继承 | 不能访问 |
公共权限 | 保护权限 | |
保护权限 | 保护权限 |
4.6.3继承中的对象模型
父类中所有的非静态成员属性都会被子类继承下去,就算是访问不到的私有成员也会继承。
4.6.4继承中的构造和析构顺序
子类继承父类之后,创建子对象的时候会调用父类的构造函数
4.6.5同名成员
如果通过子类对象,访问到父类中的同名(与子类重复)对象,需要说明作用域。
子类对象直接访问子类同名成员,需要加作用域才能访问父类成员。
4.6.6继承同名静态成员处理方式
在父类中的静态同名成员继承到子类的情况下:
子类直接访问同名成员,加作用域访问父类成员
class Fa{
public:
static int m_A;
};
int Fa::m_A = 1;
class Son:public Fa{
public:
static int m_A;
};
int Son::m_A = 2;
void test(){
Son s;
cout << s.m_A << " " << s.Fa::m_A;//2 1
}
通过域名访问:
class Fa{
public:
static int m_A;
};
int Fa::m_A = 1;
class Son:public Fa{
public:
static int m_A;
};
int Son::m_A = 2;
void test(){
cout << Son::m_A << ' ' << Son::Fa::m_A;
}
4.6.7多继承语法
一个类继承多个类
语法:class 子类: 继承方式 父类1, 继承方式 父类2......
多继承出现的同名现象需要作用域区分。
4.6.8菱形继承
两个派生类继承了同一个基类,又有一个类继承了两个派生类。
继承了两个派生类的类会同时继承到两个同样的数据,其实只需要保留一个。
可以通过虚继承来解决这个问题。
在继承权限的前面加上一个关键字:virtual
,就可以实现虚继承,这样继承的类就是序基类。
这样继承之后,同名变量只会保留一个数据
在虚拟继承之后,子类中会存在vbptr(virtual base pointer),它指向了vbtable(virtual base table虚基类表),这里记录了指针到达成员变量的偏移量
4.7多态
4.7.1多态的基本概念
分为两类:
- 静态多态:函数重载和运算符重载就是静态多态
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态的区别:
- 静态多态的函数地址早绑定,即编译阶段确定函数地址
- 动态多态的函数地址晚绑定,即运行阶段确定函数地址
观察以下代码
class Person{
public:
void speak(){
cout << "Person speaking words..." << endl;
}
};
class Kid:public Person{
public:
void speak(){
cout << "Kid speaking words..." << endl;
}
};
void __speak(Person &p){
p.speak();
}
void test(){
Kid k1;
__speak(k1);
}
output:
Person speaking words...
我们发现虽然我们用Kid类创建了对象,但是形参写的是Person类,那么在调用speak函数的时候就调用了Person类中的,这是因为静态多态地址早绑定,在编译阶段就已经确定,那么不管你传入什么对象参数,都只会按照Person类进行运行。
可以说是能够运行的函数在你代码写入形参的时候就已经注定了。
如果想要改变这种情况就需要使用动态多态。
使用方法也很简单,只需要在Person的speak函数前面加上virtual
关键字就可以了,这样speak就变为了一个虚函数。
class Person{
public:
virtual void speak(){
cout << "Person speaking words..." << endl;
}
};
class Kid:public Person{
public:
virtual void speak(){ //这里的virtual也可以不写,也能够实现同样的功能
cout << "Kid speaking words..." << endl;
}
};
class Adult:public Person{
public:
virtual void speak(){
cout << "Adult speaking words..." << endl;
}
};
void __speak(Person &p){
p.speak();
}
void test(){
Kid k1;
__speak(k1);
Adult a1;
__speak(a1);
}
output:
Kid speaking words...
Adult speaking words...
这时候地址晚绑定,只有在参数真正传入的时候才会确定去执行哪一个函数。
这样就实现了根据你所填入的参数进行情景的变换
由此引出动态多态的实现条件:
1.有继承关系
2.子类要重写过父类中的虚函数(重名)
内部原理:
在加上了virtual
关键字之后,类占用的空间变多,其中Person类里面存有一个vfptr(virtual function pointer),占用了4字节,为虚函数(表)指针,这个指针指向了一个虚函数表(vftable),这个表的内部记录了虚函数的地址,即&Person::speak()
。
在子类继承之后,并且没有写出同名函数的时候,会继承下来父类的vfptr和vftable,其中vftable也只是记录了&Person::speak()
,在我们进行写下虚函数的同名函数之后,子类里面的虚函数表里面的同名函数就会被覆盖为自己的函数,也就是将&Person::speak()
覆盖为了&Kid::speak()
。
这时候,当父类的指针或者引用指向子类对象的时候,就会发生多态,之后就会从传入的类中去找那个函数。
4.7.2 纯虚函数和抽象类
多态中,父类中的虚函数的视线通常是无意义的,主要都是调用子类重写的内容。
因此我们将虚函数改为纯虚函数。
语法:virtual 返回值类型 函数名 (参数列表) = 0;
当类中有纯虚函数之后,这个类也就被称为抽象类。
抽象类的特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
class A{
public:
virtual void func() = 0;
};
class B : public A{
public:
void func(){
cout << 1 << endl;
}
};
void test(){
B b;
b.func();
}
一个案例
// 设计抽象类
class ToMake
{
public:
virtual void BoilWater() = 0;
virtual void Brew() = 0;
virtual void PourIntoTheCup() = 0;
virtual void AddSomething() = 0;
// 注意这里不用写成虚函数,因为要继承下去进行调用
void start() // 设计一个总的函数,这样调用比较方便
{
BoilWater();
Brew();
PourIntoTheCup();
AddSomething();
}
};
class MakeCoffee : public ToMake
{
public:
void BoilWater()
{
cout << "BoilingWater\n";
}
void Brew()
{
cout << "BrewingCoffee\n";
}
void PourIntoTheCup()
{
cout << "PourIntoTheCup\n";
}
void AddSomething()
{
cout << "AddingSugarAndMilk\n";
}
};
class MakeTea : public ToMake
{
public:
void BoilWater()
{
cout << "BoilingWater\n";
}
void Brew()
{
cout << "BrewingTea\n";
}
void PourIntoTheCup()
{
cout << "PourIntoTheCup\n";
}
void AddSomething()
{
cout << "AddingLemon\n";
}
};
//设计一个能够接受所有的ToMake的子类进行工作的函数
//用指针的形式
void doWork(ToMake *abs)
{
abs->start();
delete abs;
}
void test()
{
doWork(new MakeCoffee);
doWork(new MakeTea);
}
写案例并不简单,需要有很多经验才能够写得很方便,很健全。
4.7.3虚析构和纯虚析构
多态使用的时候,会出现子类中有的属性开辟到堆区之后,父类指针在释放时无法调用到子类的析构函数。
可以通过将父类中的析构函数改成虚析构或者纯虚析构就可以了。
两者都能够实现通过父类指针释放子类对象,并且都需要有具体的函数实现。但是如果是纯虚析构,那么就无法创建对象。
语法:在析构函数语法的前面加上virtual
就可以了,如果是纯虚析构则类比上面的格式。