C++ 核心编程

堆空间开辟地址 ——— new

开辟空间

我们利用new可以在堆区开辟空间,堆区由程序员进行管控,可以手动进行管理数据存放内存的释放与否,若不写释放过程,则堆区数据会在程序结束时由OS自动释放

开辟空间形式:

1
new int(10) // 在堆上开辟了一个 int 类型的数据,new 返回的是对应数据类型的一个指针,括号内为其对应的值

如果我们想要访问该堆空间的数据我们可以创建一个对应数据类型的一个指针

1
2
int* p = new int(10); // 堆上地址保存数据在 p 中
cout << *p << endl; // 10

如果我们想要在堆上创建一个数组或者字符串呢?

1
new int[10]	// 在堆上开辟了一个 int 类型的数组,括号内为其对应的元素个数

同样的我们需要创建一个指针变量对其进行保存

1
int* arr = new int[10]; // 堆上地址保存数据在 p 中,返回数组的首地址

释放空间

我们学会了如何开辟空间后,那如何进行删除对应堆空间的数据呢?这个时候我们将会用到delete

对于不为字符串指针的我们可以直接按下面方式进行删除:

1
delete p;	// p 为指向对应堆空间的一个指针

加入是一个字符串我们需要额外加上一个中括号:

1
delete[] arr;		// [] 相当于告诉编译器这个将会释放一连串的空间地址

引用 ———— 给变量起别名

本质:给变量取别名

语法:数据类型(与原数据类型相同) &别名 = 原名

相当于将别名的地址取成了原名的地址,进而通过改变对应的内存中的数据

下面给出一个例子:

1
2
3
4
int a = 10;
int &b = a; // 使 a,b 两者的内存地址相同
b = 20;
cout << a << endl; // a 变成了 20

相关注意事项

  1. 引用必须要初始化

    需要告诉其为什么东西的别名,将其进行初始化

    1
    int &b; // 错误的
  2. 引用一旦初始化后就不可以更改了

    假设我们初始化 b 为 a 的别名后,不可以再将其进行改变为 c 的别名

    1
    2
    3
    4
    5
    int a = 10;
    int &b = a;
    int c = 20;
    b = c; // 更改 a 的值,而不是更改引用
    &b = c; // 多次引用会报错

引用做函数参数

作用:函数传参的时候,可以利用引用让形参修饰实参

优点:可以简化指针修改实参

假设我们需要完成一个交换函数,我们除了利用值传递地址传递、我们还可以使用引用传递

值传递:形参不会修饰实参

1
2
3
4
5
6
7
void swap(int a, int b){
int c ;
c = a;
a = b;
b = c;
}
// 通过这函数的时候我们将形参的值进行了改变,但是原数据呢?原数据却没有发生改变

地址传递:形参会修饰实参

1
2
3
4
5
6
7
void swap(int* a, int* b){
int c ;
c = *a;
*a = *b;
*b = c;
}
// 我们通过找到对应的地址,修改对应地址里的值而将原来的数据进行了修改

引用传递:形参会修饰实参

1
2
3
4
5
6
7
void swap(int& a, int& b){	// 传入的是引用类型
int c ;
c = a;
a = b;
b = c;
}
// 本质上还是我们通过找到对应的地址,修改对应地址里的值而将原来的数据进行了修改,使用引用可以简化对应的指针,相当于建立了另一个指针关系,其与地址的传递区别在于引用靠指针常量实现

引用做函数返回值类型

注意

  1. 不要返回局部变量的引用
1
2
3
4
5
6
7
8
9
10
11
int& t(){
int a = 10;
return a;
}
int main(){
int& ref = t();
cout << ref <<endl; // 10
// 第一次会正确输出,因为编译器进行了一次的保存
cout << ref <<endl; // ?
// 第二次输出会发生错误
}
  1. 函数的调用可以作为左值(左值: 能用于赋值运算左边的表达式)
1
2
3
4
5
6
7
8
9
10
11
12
13
int& t(){
static int a = 10; // static 保存在全局区域,其数据会在程序结束后由OS进行释放
return a;
}
int main(){
int& ref = t();
cout << ref <<endl; // 10
cout << ref <<endl; // 10

t() = 100; // 因为返回的是一个 a 的引用,这个函数的调用可以作为左值,相当于我们可以通过这个方式来对返回值进行修改
cout << ref <<endl; // 100
cout << ref <<endl; // 100
}

引用本质

其在C++内部实现是一个指针常量( 算是一种懒人版指针? )

1
2
int& ref = a;    // 会被编译器转化为  int* const ref = a ;
ref = 20; // 编译器发现 ref 为引用,会帮我们进行转换为 *ref = 20;

常量引用

作用:用来修饰形参,来防止误操作

1
2
3
4
5
int& ref = 10; // 错误,引用必须引用到一块合法的内存空间
// -----
const int& ref = 10; // 正确,加上 const 后,编译器将代码进行修改为 int temp = 10;const int& ref = temp;
// 加上 const 后变为只读不可以修改
ref = 20; // 错误

函数提高

函数默认参数

C++中,函数的形参列表中的形参是可以有默认值的

语法:返回值类型 函数名(形参 = 默认参数)

正常情况下我们计算一个三个数相加时,需要将三个参数均传入

1
2
3
4
5
6
int func(int a,int b,int c){
return a+b+c;
}
int main(){
func(1,2,3); // 1 + 2 + 3
}

但是如果我们设定了默认值我们可以只传入一部分参数,其他数据使用默认值

1
2
3
4
5
6
int func(int a,int b = 10,int c = 30){    
return a+b+c;
}
int main(){
func(1,2); // 1 + 2 + 30
}

注意事项:

  1. 当一个形参设置的默认参数时,其后面的形参都需要设置对应的默认参数

  2. 如果函数的声明有默认参数,函数实现就不能有默认参数

    1
    2
    3
    4
    int func(int a = 10,int b = 10);
    int func(int a = 20 ,int b = 20){ // 默认参数重定义了,会发生报错
    return a + b;
    }

函数占位参数

语法:返回值类型 函数名 (数据类型){}

1
2
3
void Func( int a ,int ){
//XXX
}

其中占位参数可以拥有默认参数

1
2
3
void Func( int a ,int = 10){
//XXX
}

函数重载

作用:函数名可以相同,提高复用性

函数重载满足条件:

  • 同一个作用域下
  • 函数名相同
  • 函数参数类型不同或者个数不同或者顺序不同

注意:函数的返回值不可以作为函数重载的条件

1
2
3
4
5
6
7
8
9
10
11
void func(){
//XXX
}
void func(int a){
//XXX
}

int main(){
func(); // 调用第一个func()
func(10); // 调用第二个func()
}

注意事项:

  • 引用作为重载条件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void func(int& a){
    //XXX
    }
    void func(const int& a){ // const 限制了只读的特点
    //XXX
    }

    int main(){
    int a = 10;
    func(a); // 调用第一个func()
    func(10); // 调用第二个func(),编译器会产生一个中间变量保存10,之后调用函数
    }
  • 函数重载遇到默认参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void func(int a){
    //XXX
    }
    void func(int a,int b = 10){
    //XXX
    }

    int main(){
    func(10); // 发生错误,两个函数均可以调用,产生了二义性
    }

类和对象

C++面向对象的三大特性为:封装、继承、多态

封装

意义:

  • 将属性和行为作为一个整体,表现生活中的事物

  • 将属性和行为加以权限控制

语法:

1
2
3
4
5
6
7
8
9
10
class 类名{
//访问权限
//public: //公共权限 类内可以访问 类外可以访问
//protected: //保护权限 类内可以访问 类外不可以访问
//private: //私有权限 类内可以访问 类外不可以访问
//属性
// XXX
//行为 (通常为函数)
// XXX
};

如定义一个圆类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define PI 3.1415
class cir{
public: //访问权限:公共权限
int m_r; //属性
double calculate(){ //行为
return 2 * PI * m_r;
}
};

int main(){
cir c1; // 通过类创建一个对象,来进行实例化
c1.m_r = 10;
cout << c1.calculate() << endl; // 输出计算的计算周长
}

classstruct的唯一的区别就在于默认访问权限,class在不注明对应的权限时,默认为私有struct默认权限为公共

对象的初始化与清理

C++利用构造函数析构函数解决对象的初始化和清理,这两个函数将会被编译器自动调用,完成对象的初始化和清理工作,如果我们不提供构造和析构函数,编译器会提供,但是其提供的构造和析构函数是空实现。

  • 构造函数:主要用于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用
  • 析构函数:主要用于对象销毁前系统自动调用,执行一些清理工作。

构造函数语法: 类名(){}

  1. 构造函数,没有返回值也不写void
  2. 函数名称与类名相同
  3. 构造函数可以有参数,因此可以发生重载
  4. 程序在调用对象时候会自动调用构造函数,无须手动调用,而且只会调用一次

析构函数语法:~类名(){}

  1. 析构函数,没有返回值也不写void
  2. 函数名称与类名相同,在名称前加上符号~
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次

构造函数的分类及调用

两种分类方式

按参数分为: 有参构造和无参构造

按类型分为: 普通构造和拷贝构造

三种调用方式:括号法、显示法、隐式转换法

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
60
61
62
//1、构造函数分类
// 按照参数分类分为 有参和无参构造 无参又称为默认构造函数
// 按照类型分类分为 普通构造和拷贝构造

class Person {
public:
//无参(默认)构造函数
Person() {
cout << "无参构造函数!" << endl;
}
//有参构造函数
Person(int a) {
age = a;
cout << "有参构造函数!" << endl;
}
//拷贝构造函数
Person(const Person &p) {
// 将传入的人身上的所有属性,拷贝到我身上
age = p.age;
cout << "拷贝构造函数!" << endl;
}
//析构函数
~Person() {
cout << "析构函数!" << endl;
}
public:
int age;
};

//2、构造函数的调用

void test01() {
Person p1; //调用无参构造函数
}

void test02() {

//2.1 括号法,常用
Person p2(10); //调用有参构造函数
Person p3(p2); //调用拷贝构造函数
//注意1:调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明
//Person p2(); // 错误

//2.2 显式法,将后面的匿名对象保存到左边的对象处
Person p2 = Person(10);
Person p3 = Person(p2);
//Person(10); 单独写就是匿名对象,当前行结束之后,将会马上析构

//注意2:不能利用拷贝构造函数初始化匿名对象,编译器认为是对象声明
//Person (p3); // 错误

//2.3 隐式转换法
Person p4 = 10; // Person p4 = Person(10);
Person p5 = p4; // Person p5 = Person(p4);
}

int main() {
test01();
test02();
system("pause");
return 0;
}

深拷贝与浅拷贝

浅拷贝 -> 简单的赋值拷贝操作

深拷贝 -> 在堆区重新开辟空间,进行拷贝操作

浅拷贝带来的问题是堆区内存的重复释放,对此问题我们采用深拷贝来进行解决

如果属性有在堆区开辟的,一定要自己提供一个构造函数,防止浅拷贝带来的问题

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person{
public:
Person(){
//XXX
}
Person(Person &p){ // 我们自己创建的一个拷贝构造函数
a = p.a;
b = new int(*p.b);
}
~Person(){
if(b != NULL){
delete b;
}
}
public:
int a;
int* b; //此时我们创建了一个堆上变量,需要我们自己写一个拷贝构造函数
};

上面例子中因为b是一个指针,指向一个地址空间,如果我们简单的将对应的堆上地址进行复制过去时便会产生在析构函数中多次进行释放的一个问题,从而导致问题

初始化列表

语法: 构造函数(): 属性1(值1)、属性2(值2)...

我们传统的初始化过程像下面代码:

1
2
3
4
5
6
7
8
9
class Person(){
Person(int a,int b,int c){
ma = a;
mb = b;
mc = c;
}
public:
int ma,mb,mc;
};

利用C++的一个特性我们可以将其进行修改为:

1
2
3
4
5
6
7
class Person(){
Person(int a,int b,int c):ma(a),mb(b),mc(c){
// XXX
}
public:
int ma,mb,mc;
};

类对象作为类成员

C++类中的成员可以是另一个类的对象,我们称该成员为对象成员

如:

1
2
3
4
class A {};
class B {
A a;
};

可以简单的理解为结构体的嵌套,一个类中包括另一个类也是相似的

需要注意的是当其他类的对象作为类成员时,会先调用类成员,再构造自身,以上面的例子来说,先会执行A的构造函数,随后执行B的构造函数。而执行析构时则先执行B,随后再执行A

静态成员

静态成员就算在成员变量和成员函数前加上static

静态成员变量:

  • 所有对象共享同一份数据

    当存在有另一个类名相同的类对类中一个静态成员进行修改时,那么原先类中的值将会发生改变

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class P{
    static int a;
    }
    int P::a = 100;

    void test(){
    P p0,p1;
    p1.a = 200;
    cout<<p0.a; // 200
    }
    int main(){
    test();
    };

    于此同时我们可以通过对象或者类名来进行访问对应的内存(静态成员变量)

    1
    2
    3
    4
    5
    // 对象
    P p;
    cout << p.a << endl;
    // 类名
    cout << P::a << endl;
  • 在编译阶段分配内存

  • 类内声明,类外初始化

    假如我们定义了一个类

    1
    2
    3
    class P{
    static int a; //类内声明
    };

    那么我们还需要在类外进行初始化

    1
    int P::a = 100;

    那么结合起来整个定义过程为:

    1
    2
    3
    4
    class P{
    static int a; //类内声明
    };
    int P::a = 100;

静态成员函数:

  • 所有对象共享同一个函数

  • 静态成员函数只能访问静态成员变量

    举个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class P{
    static void func(){
    a = 100; // 可以进行操作
    b = 100; // 非法操作
    }
    static int a = 100;
    int b;
    };
    int P::a = 0;

    a作为静态成员时可以被其访问到,而b作为一个普通变量无法被访问到

C++ 对象模型和this指针

成员变量和成员函数分开存储

C++中,类内成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上

空对象占用内存空间为 1,因为C++编译器会给每一个空对象也分配一个字节空间,是为了区分空对象占内存的位置

不为空时,类的大小就算类内成员所占空间的大小

需要注意的是,仅有非静态成员变量属于类的对象上,像静态成员变量、非静态成员函数、静态成员函数都不属于类对象上

1
2
3
4
5
6
class P{
int a;
static int b;
void func1(){}
static func2(){}
};

此时P类的大小还是只计算一个int a的大小,即为 4

this指针

C++中成员变量和成员函数是分开存储的,每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码

C++通过this指针指向被调用的成员函数所属的对象

this指针式隐含每一个非静态成员函数内的一种指针,this指针不需要定义,直接使用即可

this指针用途:

  • 当形参与成员变量同名时,可以用this指针来区分

    举个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Person{
    public:
    Person(int age){
    age = age;
    }
    int age;
    };
    int main(){
    Person p1(18)
    cout<<p1.age;
    }

    我们可以发现程序的输出并不是18,而是一个随机的值,为了解决中国问题我们可以使用this指针来指向被调用的成员函数所属的对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Person{
    public:
    Person(int age){
    this -> age = age;
    }
    int age;
    };
    int main(){
    Person p1(18)
    cout<<p1.age;
    }

    也就是this指向本类中的age变量来避免了重名的问题

  • 在类的非静态成员函数中返回对象本身,可使用return *this

    举个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Person{
    public:
    Person(int age){
    this -> age = age;
    }
    void PersonAgeAdd(Person &p){
    this -> age += p.age;
    }
    int age;
    };
    int main(){
    Person p1(18)
    Person p2(10)
    p1.PersonAgeAdd(p2)
    cout<<p1.age;
    }

    上面代码是我们一般实现一个年纪加法的一种运算,对此我们如果想要再次实现一个加法需要再加一行,再加上一行的p1.PersonAgeAdd(p2)

    但是C++提供的this指针,可以给我们创建点便利,像Python代码一样使用多个.来进行链接,达到p1.PersonAgeAdd(p2).PersonAgeAdd(p2)的效果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Person{
    public:
    Person(int age){
    this -> age = age;
    }
    Person& PersonAgeAdd(Person &p){ // 需要使用引用的方式来进行返回
    this -> age += p.age;
    return *this; // 返回的是Person本身这个实体
    }
    int age;
    };
    int main(){
    Person p1(18)
    Person p2(10)
    p1.PersonAgeAdd(p2).PersonAgeAdd(p2)
    cout<<p1.age;
    }

    需要注意的是,我们如果将其不小心写成了一个值的返回

    1
    2
    3
    4
    Person PersonAgeAdd(Person &p){ // 引用的方式来进行返回被我们更改为一个值的类型
    this -> age += p.age;
    return *this; // 返回的是不再是Person本身这个实体,而是创建了一个新的Person实体,可以理解为Person’
    }

    我们如果是上面的写法的话,那我们得到的永远是Person的一个新实例,而不是原先的对象本身,导致我们想获取到的数据发生问题

空指针访问成员函数

C++空指针也是可以调用成员函数的,但是也要注意到有没有用到this指针

如果用到了this指针,需要加以判断保证代码健壮性

空指针可以访问对应的对象,但是不能访问对象中的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person{
public:
void test1(){
cout << "test1" << endl;
}
void test2(){
cout << age <<endl;
// 此处的 age 等价于 this -> age
}
int age;
};

int main(){
Person* P = NULL;
P -> test1(); // 不会产生报错
p -> test2(); // 编译不报错,运行报错
}

上面例子中,因为我们没有对this指针进行判断,当我们新创建的一个P指针指向对应age变量时,引发了读取访问权限异常

对此我们可以加入一个判断,来确保this指针非空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person{
public:
void test1(){
cout << "test1" << endl;
}
void test2(){
if(this == NULL){
return;
}
cout << age <<endl;
// 此处的 age 等价于 this -> age
}
int age;
};

const修饰成员函数

常函数

  • 成员函数后加const后,我们称这个函数为常函数

    成员函数后面加const,修改的是this指向,让指针指向的值也不可以修改

  • 常函数内不可以修改成员属性

  • 成员属性声明时加关键字mutable后,常函数中依然可以修改

    假如我们有一个类

    1
    2
    3
    4
    5
    6
    class Person{
    void test() const {
    this->age = 20;
    }
    int age;
    };

    可以看到this->age = 20;处会发生问题,显示无法修改,假如我们要实现修改什么班呢,我们可以加上mutable

    1
    2
    3
    4
    5
    6
    class Person{
    void test() const {
    this->age = 20;
    }
    mutable int age;
    };

    此时我们便可以进行对应的修改

常对象

  • 声明对象前加const称该对象为常对象
  • 常对象只能调用常函数

友元

友元的目的就是让一个函数或者类,访问另一个类中的私有函数

关键字:friend

友元的实现

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元

全局函数做友元

我们创建一个全局函数,之后将其在类中进行声明加上friend即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person{
friend void F(Peroson &p);
public:
Person(){
age = 18;
}
private:
int age;
};

void F(Peroson &p){
cout<< p->age <<endl;
}
int main(){
Person p;
F(p);
}

类做友元

与全局函数做友元一样,我们同样创建一个类之后,在目的类中加上一个friend声明即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person;
class F{
void test(Peroson &p){
cout << p -> age <<endl;
}
}
class Person{
friend class F;
public:
Person(){
age = 18;
}
private:
int age;
};

成员函数做友元

和前面的方式一样,我们在对应的类中加上对应的作用域以及friend即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person;
class F{
public:
void test(){};
private:
Person* p;
}

class Person{
friend void F::test;
public:
Person(){
age = 18;
}
private:
int age;
};

运算符重载

对已有的运算符重新进行定义,赋予另一种功能以适应不同的需要

关键字:operator

我们可以通过operator来完成一种类型的变量和另一种类型的变量相互运算

加法运算重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person{
public:
Person(){}
Person(int a){
this->age = a;
}
Person operator + (Person &p){
Person temp;
temp.age = this->age + p.age;
return temp;
}
public:
int age;
};

左移运算重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person{
public:
Person(){}
Person(int a){
this->age = a;
}
public:
int age;
};

ostream & operator <<(ostream &cout,Person &p){
cout<<p.age;
return cout;
}

递增运算符重载

重载前置++运算符
1
2
3
4
5
6
7
8
9
10
11
12
13
class MyInteger{
public:
MyInteger(){
num = 0;
}
MyInteger & operator ++ (){
num++;
return *this;
}
private:
int num;
};

前置++时,++在前,先完成的是自增运算,随后进行赋值,因此我们输出的结果是自增后的值

重载后置++运算符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyInteger{
public:
MyInteger(){
num = 0;
}
MyInteger operator ++ (int){ // int 代表占位参数
// 需要先记录当时结果,后递增最后将记录做返回
MyInteger temp = *this;
num++;
return *temp;
}
private:
int num;
};

后置++时,++在后,先完成的是输出值,随后进行自增,因此我们输出的结果是自增前的值

赋值运算符重载

C++编译器至少给一个类添加 4 个函数

  1. 默认构造函数
  2. 默认析构函数
  3. 默认拷贝函数,对属性进行值拷贝
  4. 赋值运算符operator=对属性进行值拷贝

如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person{
public:
Person(int age){
this->age = new int(age)
}
~Person(){
if(age!=NULL){
delete age;
age = NULL;
}
}
Person& operator = (Person &p){
if (age!=NULL){
delete age;
age = NULL;
}
age = new int(*p.age);
return *this;
}
int* age;
};

关系运算符重载

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
class Person{
public:
Person(int age){
this->age = new int(age)
}
~Person(){
if(age!=NULL){
delete age;
age = NULL;
}
}
bool operator == (Person &p){
if (this->age==p.age){
return true;
}

return false;
}
bool operator != (Person &p){
if (this->age==p.age){
return false;
}

return true;
}
int age;
};

函数调用运算符重载

  • 函数调用()也可以重载

  • 用于重载后使用的方式非常像函数的的调用,因此称为仿函数

  • 仿函数没有固定写法,非常灵活

简单的加法实现
1
2
3
4
5
6
7
8
9
10
class MyAdd(){
int operator()(int num1,int num2){
return num1+num2;
}
};
int main(){
MyAdd myadd;
int ret = myadd(100,100);
cout<<ret;
}
使用匿名函数对象实现
1
2
3
4
5
6
7
8
class MyAdd(){
int operator()(int num1,int num2){
return num1+num2;
}
};
int main(){
cout<<MyAdd()(100,100);
}

继承

基本语法

语法:class 子类 : 继承方式 父类

子类也称为派生类,父类也称为基类

1
2
3
4
5
6
7
8
9
10
11
12
13
class YYY{
public:
void common(){
// XXX
}
};
class XXX : public YYY
{
public:
void special(){
cout << "someting special"<<endl;
}
};

继承可以减少重复的代码,派生类中的成员包含两大部分:

  • 一类是从积累继承过来的,一类是自己增加的成员

  • 从基类继承过来的表现其共性,而新增的成员体现了其个性

继承方式

继承方式有三种:公共继承、保护继承、私有继承

继承中的对象模型

父类中的私有成员只是被隐藏了,但是还是会继承下去

我们可以通过VS的开发人员命令提示符,来大型我们想要查看的类的结构

1
cl /d1 reportSingleClassLayout<class name> <file name>

继承中构造和析构顺序

子类继承父类后,当创建子类对象,也会调用父类的构造函数

那么便会产生一个问题,父类与子类的构造函数和析构函数是谁先谁后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base{
public:
Base(){
cout<<"1"<<endl;
}
~Base(){
cout<<"2"<<endl;
}
};

class Son:public Base(){
public:
Son(){
cout<<"3"<<endl;
}
~Son(){
cout<<"4"<<endl;
}
};

我们构造上面的例子我们调用Son类型时,其输出的值顺序为1342,说明继承中先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反

继承中同名成员处理方式

当父类与子类中出现同名的成员我们进行访问对应成员时需要注意:

访问子类同名成员直接访问即可,当访问父类同名成员时需要加上作用域

1
<子类名>.<父类名>::<参数>

如果子类中出现了和父类同名的成员函数,子类的同名成员会隐藏掉父类中所所有同名成员函数

继承中同名静态成员处理方式

与同名非静态成员的处理方式一样,访问子类同名成员直接访问即可,当访问父类同名成员时需要加上作用域

我们可以通过对象方式进行访问或者我们可以通过类名加上对应作用域来进行访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class F{
public:
int a;
};
class S:public F{
public:
int b;
}
int main(){
S s;
// 通过对象访问
cout<<s.b<<endl;
cout<<s.F::a<<endl;

// 通过类名访问
cout<<S::b<<endl;
// 第一个::代表通过类名访问,第二个::代表访问父类作用域下
cout<<S::F::a<<endl;
}

对于成员同名静态成员函数,也会隐藏父类中所有同名成员函数,如果想访问父类中静态成员函数,需要同样加上对应作用域

多继承语法

语法:class 子类:继承方式 父类1 , 继承方式 父类2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class BaseA{
public:
BaseA(){
a = 100;
}
int a;
};
class BaseB{
public:
BaseB(){
b = 200;
}
int b;

};
class Son:public BaseA,public BaseB{
public:
int s;
}
int main(){
cout<<Son::s<<endl;
cout<<Son::BaseA::a<<endl;
cout<<Son::BaseB::b<<endl;
}

菱形继承

概念:两个派生类继承同一个基类,又有某个类同时继承两个派生类,这种继承被称为菱形继承

当我们继承了两个类时,我们可以意识到这两个类都继承了基类,那么基类便会有重复,因此我们需要解决这个问题,我们可以采用虚继承来解决这个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Animal{ // 虚基类
pubilic:
int age;
};
// 利用虚继承来解决菱形继承的问题
class camel:virtual Animal{};
class sheep:virtual Animal{};
class alpaca:public camel,public sheep{};

int main(){
alpaca T;
T.sheep::age = 10;
T.camel::age = 20;
cout << T.sheep::age <<endl;
cout << T.camel::age <<endl;
}

多态

多态优点:

  • 代码组织结构清晰
  • 可读性强
  • 利于前期和后期的扩展以及维护

多态分为两类:

  • 静态多态:函数重载和运算符重载属于静态多态,复用函数名
  • 动态多态:派生类和虚函数实现运行时多态

两者区别:

  • 静态多态的函数地址早绑定 - 编译阶段确定函数地址
  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Animal{
pubulic:
void speak(){
cout << "saying" <<endl;
}
};
class cat:public Animal{
public:
void speak(){
cout << "cat is saying" <<endl;
}
};

void doSpeak(Animal &annimal){
animal.speak();
}
int main(){
Cat cat;
doSpeak(cat); // C++ 中允许父子类型的直接转换
}

上述代码中可以显示出的是saying,这是因为执行说话的函数地址早绑定,在编译阶段就可以确定函数的地址,加入我们想要让输出的是cat is saying那么这个函数的地址就不能提前绑定,需要在运行阶段中进行晚绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Animal{
pubulic:
// 函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了
virtual void speak(){ // 使用虚函数来进行晚绑定
cout << "saying" <<endl;
}
};
class cat:public Animal{
public:
virtual void speak(){
cout << "cat is saying" <<endl;
}
};

void doSpeak(Animal &annimal){
animal.speak();
}
int main(){
Cat cat;
doSpeak(cat); // C++ 中允许父子类型的直接转换
}

动态多态需要满足有继承关系,子类要重写父类的虚函数

动态多态使用,父类的指针或者引用指向子类对象

多态原理分析

当我们使用virtual关键字时,其会创建一个虚函数(表)指针 ———— vfptr占4字节

我们子类继承使用virtual时便会将父类中的虚函数表指针进行替换,替换成子类自己的函数指针,因此当父类的指针或者引用指向子类对象的时候发生多态

纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容

因此我们可以将虚函数改为纯虚函数,对应语法:virtual 返回值类型 函数名 (参数列表) = 0,当类中有了存虚函数,这个类也称为抽象类

抽象类特点

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类

如:

1
2
3
4
5
6
7
8
9
10
class Base{
public:
// =0 为纯说明符,不可以改成其他的
virtual void func() = 0;
};
//子类必须重写父类中的纯虚函数,否则无法实例化对象
class Son{
public:
virtual void func(){};
};

可以理解为我们父类提供一个指向,需要通过子类来进行实现具体的过程,因此我们在每次扩展功能时不需要去修改原来的代码,仅需要往后扩展即可。

虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

解决方式:将父类中的析构函数改为虚析构或者纯虚析构

虚析构和纯虚构共性:

  • 可以解决父类指针释放子类对象
  • 都需要有具体的函数实现

虚析构和纯系析构区别:如果是纯虚析构,该类属于抽象类,无法实例化对象

虚析构语法:virtual ~类名(){}

虚析构语法:virtual ~类名() = 0; 类名::~类名(){}

虚析构和纯虚构函数都必须有相应实现

父类指针在析构的时候不会调用子类的析构函数,因此父类指针释放子类对象时会存在不干净的问题。

虚析构和纯虚析构就是用来解决通过父类指针释放子类对象

如果子类中没有堆区数据,可以不写为虚析构或纯虚析构

用于纯虚析构函数的类也属于抽象类

文件操作

C++中的文件操作需要包含头文件#include <fstream>

操作文件的三大类:

  1. ofstream:写操作
  2. ifstream:读操作
  3. fstream:读写操作

创建流对象:ofstream ofs

打开文件:ofs.open("<file path>",<mode>)

打开方式 解释
ios::in 读文件
ios::out 写文件
ios::ate 初始位置:文件尾
ios::app 追加方式写文件
ios::trunc 如果文件存在先删除,再创建
ios::binary 二进制方式

注意:文件打开方式可以配合使用,使用|操作符

写数据:ofs << "XXX";

关闭文件:ofs.close()

读文件

使用>>

1
2
3
4
5
6
ifstream ifs;
ifs.open("xxx",ios::in);
char buff[1024]={0};
while(ifs >> buff){
cout << buff << endl;
}

使用getline()

1
2
3
4
5
6
ifstream ifs;
ifs.open("xxx",ios::in);
char buff[1024]={0};
while(ifs.getline(buf,sizeof(buff))){
cout << buff << endl;
}

或者

1
2
3
4
5
6
ifstream ifs;
ifs.open("xxx",ios::in);
char buff[1024]={0};
while(getline(ifs,buff)){
cout << buff << endl;
}

使用get()

1
2
3
4
5
6
ifstream ifs;
ifs.open("xxx",ios::in);
char buff[1024]={0};
while((c = ifs.get()) != EOF ){
cout << c << endl;
}

C++ 核心编程
https://equinox-shame.github.io/2022/09/11/C++ 核心编程/
作者
梓曰
发布于
2022年9月11日
许可协议