C++核心编程部分总结

ZJ Lv100

前言

基于已经学习过C语言/C++基础部分的进阶部分,主要是面向对象的提升

1 内存存放

1.1 变量的存放区域

局部变量和局部常量存放在当前函数的栈区

全局变量和静态变量都在全局区

常量在接近全局区的常量区

image-20241023164153955

代码实现案例

1545017602518

即:

  • 全局区中存放全局变量、静态变量、常量
  • 常量区中存放 const修饰的全局常量 和 字符串常量

1.2 堆区

在C++中可以利用关键词new在堆区开辟内存

1
2
3
4
5
6
7
int* func()
{
int* a = new int(10);//开辟单个数据
return a;
}

int* arr = new int[10];//开辟整个数组

这里面的a指针就存放在栈区与全局区外的堆区,可以理解为另一个存放变量的地方

new:就是将值进行拷贝,并将地址在堆区新开辟一个,通过访问地址就可以访问值了

堆区:由程序员分配释放,若程序员不释放,程序结束时由操作系统回收

在C++中可以利用关键词delete手动释放在堆区开辟的内存,释放数组的时候需要加上[],delete[] 数组名;

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
//开辟数
int* func()
{
int* a = new int(10);
return a;
}

int main() {

int *p = func();

cout << *p << endl;
cout << *p << endl;

//利用delete释放堆区数据
delete p;

//cout << *p << endl; //报错,释放的空间不可访问
//开辟数组
int* arr = new int[10];

for (int i = 0; i < 10; i++)
{
arr[i] = i + 100;
}

for (int i = 0; i < 10; i++)
{
cout << arr[i] << endl;
}
//释放数组 delete 后加 []
delete[] arr;

system("pause");

return 0;
}

2 引用类型

2.1 引用概念

引用类型&

**作用: **给变量起别名,在函数中引用是可以作为函数的返回值存在的

语法: 数据类型 &别名 = 原名

本质:内容的指针常量,int& b=a; 等于 int* const b =&a;,前者表示给a起别名为b,后者表示给a的地址新定义为一个指针常量b,则用星号解析b的时候就等于a,而在c++中,别名后想要用该别名,就不用每次都星号解析,直接用即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//发现是引用,转换为 int* const ref = &a;
void func(int& ref){
ref = 100; // ref是引用,转换为*ref = 100
}
int main(){
int a = 10;
//自动转换为 int* const ref = &a;
//指针常量是指针指向不可改,也说明为什么引用不可更改
int& ref = a;
ref = 20; //内部发现ref是引用,自动帮我们转换为: *ref = 20;

cout << "a:" << a << endl;
cout << "ref:" << ref << endl;

func(a);
return 0;
}

2.2 注意事项

  • 引用必须初始化

  • 引用在初始化后,不可以改变

  • 在函数中不能返回局部变量引用

    如果在函数中返回了局部变量引用,第一次系统会默认调用局部变量,是为了防止出错,而第二次则会传入随机地址,表示栈区没有对应的值(目前的最新ide已经不支持了)

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
int a = 10;
int b = 20;
//int &c; //错误,引用必须初始化
int &c = a; //一旦初始化后,就不可以更改
c = b; //这是赋值操作,不是更改引用
//即b赋值给c和a,而不是将别名改为b
//-------------------------------------------------
//返回局部变量引用
int& test01() {
int a = 10; //局部变量
return a;
}

//返回静态变量引用
int& test02() {
static int a = 20; //静态变量,存在全局区
return a;
}
int mian(){

//不能返回局部变量的引用
int& ref = test01();
cout << "ref = " << ref << endl;
//第一次可以被调用(目前的最新ide已经不支持了)
cout << "ref = " << ref << endl;//第二次不行

//如果函数做左值,那么必须返回引用
int& ref2 = test02();
test02() = 1000;
cout << "ref2 = " << ref2 << endl;//两次都是1000
cout << "ref2 = " << ref2 << endl;//两次都是1000
//因为将值传给了引用类型函数
//则test02() = 1000;就等于a=1000,是可以被执行的

return 0;
}

2.3 引用与函数

2.3.1 引用传递

当函数中的参数为引用类型时,即表示当传入参数时,参数被别名,当修改别名的内容时,原传入的参数也会一起被修改

1
2
3
4
5
6
//引用传递
void mySwap03(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}

在平时使用的过程中,也可以用过传入指针来通过改变指针,来改变值

1
2
3
4
5
6
//地址传递
void mySwap02(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}

但是不可以传入空的值,这样表示单独在函数中开辟的新局部变量,不会直接影响传入的参数

1
2
3
4
5
6
//值传递
void mySwap01(int a, int b) {
int temp = a;
a = b;
b = temp;
}

通过引用参数产生的效果同按地址传递是一样的。引用的语法更清楚简单

2.3.2 引用返回值

引用还可以作为函数返回值被赋值,即作为左值

1
2
3
4
5
6
int& test() {
static int a = 20;
return a;
}

test() = 1000;

2.3.3 常量引用

通过const修饰引用形参,防止形参改变实参

用在非函数场景是可以防止变量被朽败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//引用使用的场景,通常用来修饰形参
void showValue(const int& v) {
//v += 10;
cout << v << endl;
}

int main() {

//int& ref = 10; 引用本身需要一个合法的内存空间,因此这行错误
//加入const就可以了,编译器优化代码,int temp = 10; const int& ref = temp;
const int& ref = 10;

//ref = 100; //加入const后不可以修改变量
cout << ref << endl;

//函数中利用常量引用防止误操作修改实参
int a = 10;
showValue(a);

system("pause");

return 0;
}

3 函数进阶

3.1 函数传参

3.1.1 占位参数

当函数传入的参数没有具体字面量时,表示占位参数,可以在类中的构造函数中起到作用

1
2
3
4
5
6
7
8
9
10
void test(int a,int)
{
return a;
}
int main() {

test(11,1); //占位参数必须填补

return 0;
}

3.1.2 默认参数

函数包含参数,并给参数赋值,则在函数调用时可以不用传入那个参数的值

1
2
3
4
5
6
7
8
9
10
void func2(int a, int b = 10)
{
cout << "func2(int a, int b = 10) 调用" << endl;
}
int main(){

fun2(1);//只传入一个参数即可

return 0;
}

3.1.3 防修改参数

当函数参数中加入const关键词时,可以防止参数被修改

1
2
3
4
5
void test(const int& a)
{

}

3.2 函数重载

概念:将函数命重复利用,但效果不同,即实现重载,但不覆盖

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

函数重载满足条件:

  • 同一个作用域下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //函数同在全局作用域
    void func()
    {
    cout << "func 的调用!" << endl;
    }
    void func(int a)
    {
    cout << "func (int a) 的调用!" << endl;
    }
  • 函数名称相同

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //函数名相同
    void func()
    {
    cout << "func 的调用!" << endl;
    }
    void func(int a)
    {
    cout << "func (int a) 的调用!" << endl;
    }
  • 函数参数类型不同 或者 个数不同 或者 顺序不同

    引用类型和指针类型也是类型不同

    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
    void func()
    {
    cout << "func 的调用!" << endl;
    }
    void func(int a)
    {
    cout << "func (int a) 的调用!" << endl;
    }
    void func(int a ,int b)//函数参数个数不同
    {
    cout << "func (int a ,int b) 的调用!" << endl;
    }
    void func(int a ,int& b)//函数参数类型不同
    {
    cout << "func (int a ,int& b) 的调用!" << endl;
    }
    void func(int a ,int* b)//函数参数类型不同
    {
    cout << "func (int a ,int* b) 的调用!" << endl;
    }
    void func(int a ,double b)//函数参数类型不同
    {
    cout << "func (int a ,double b) 的调用!" << endl;
    }
    void func(double a ,int b)//函数参数类型相同,顺序不同
    {
    cout << "func (double a ,int b)的调用!" << endl;
    }

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

1
2
3
4
5
6
7
8
9
10
11
void func(double a ,int b)
{
cout << "func (double a ,int b)的调用!" << endl;
}

//函数返回值不可以作为函数重载条件
//下面的函数返回值为int,上面返回void,但不能重载
//int func(double a, int b)
//{
// cout << "func (double a ,int b)的调用!" << endl;
//}

当函数重载碰到函数默认参数,会产生歧义,尽量避免发生

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//函数重载碰到函数默认参数

void func2(int a, int b = 10)
{
cout << "func2(int a, int b = 10) 调用" << endl;
}

void func2(int a)
{
cout << "func2(int a) 调用" << endl;
}

int main() {

//func2(10); //碰到默认参数产生歧义,需要避免

return 0;
}

image-20241023174249965

4 类与对象*

4.1 类的封装

4.1.1 封装的概念

封装的意义:

  • 将属性和行为作为一个整体,表现生活中的事物
  • 将属性和行为加以权限控制

4.1.2 类封装的表现形式

类关键词:class

封装语法: class 类名{ 访问权限: 属性 / 行为 };

属性:即类中的变量

行为(方法):即类中的函数

4.1.3 封装中属性和行为的权限

  1. public 公共权限 —— 类内可以访问 类外可以访问
  2. protected 保护权限 —— 类内可以访问 类外不可以访问(继承的可以访问)
  3. private 私有权限 —— 类内可以访问 类外不可以访问(仅自己或者友元可以访问)

public中的函数可以调用并修改private中的成员属性

4.1.4 类与结构体的区别

在C++中 struct和class唯一的区别就在于 默认的访问权限不同

区别:

  • struct 默认权限为公共
  • class 默认权限为私有

4.2 类的组成

即对象初始化

4.2.1 构造函数与析构函数*

与php中的类似

image-20241024014451448

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

当不主动提供构造和析构时,编译器会提供,但编译器提供的构造函数和析构函数是空实现即不返回任何指令。

一般是先在对象初始化之前执行构造函数,在执行后随着当前栈一起释放后执行析构函数

函数语法

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

  1. 构造函数,没有返回值也不写void
  2. 函数名称与类名相同
  3. 构造函数可以有参数,因此可以发生重载
  4. 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
1
2
3
4
5
6
7
8
9
class Person
{
public:
//构造函数
Person()
{
cout << "Person的构造函数调用" << endl;
}
};

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

  1. 析构函数,没有返回值也不写void
  2. 函数名称与类名相同,在名称前加上符号 ~
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
1
2
3
4
5
6
7
8
9
class Person
{
public:
//析构函数
~Person()
{
cout << "Person的析构函数调用" << endl;
}
};

4.2.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
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
63
64
//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 p; //调用无参构造函数
}

//调用有参的构造函数
void test02() {

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

//2.2 显式法
Person p2 = Person(10);
Person p3 = Person(p2);
//Person(10)单独写就是匿名对象 当前行结束之后,马上析构

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

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

int main() {

test01();
//test02();

system("pause");

return 0;
}
4.2.2.1 拷贝构造函数
4.2.2.1.1 概念
1
2
3
Person(const Person& p) {
cout << "拷贝构造函数!" << endl;
}

拷贝构造需要在原对象的基础上进行拷贝,则需要const使原对象不发生变化,而为了不反复使系统调用临时变量(其中又会调用一次对象,而对象里面又有拷贝构造函数会造成无限递归)复制传入的实参,又需要使用引用&来进行限制

4.2.2.1.2 调用时机

C++中拷贝构造函数调用时机通常有三种情况

示例类创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Person {
public:
Person() {
cout << "无参构造函数!" << endl;
mAge = 0;
}
Person(int age) {
cout << "有参构造函数!" << endl;
mAge = age;
}
Person(const Person& p) {
cout << "拷贝构造函数!" << endl;
mAge = p.mAge;
}
//析构函数在释放内存之前调用
~Person() {
cout << "析构函数!" << endl;
}
public:
int mAge;
};

  • 使用一个已经创建完毕的对象来初始化一个新对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //1. 使用一个已经创建完毕的对象来初始化一个新对象
    void test01() {

    Person man(100); //man对象已经创建完毕
    Person newman(man); //调用拷贝构造函数
    Person newman2 = man; //拷贝构造

    //Person newman3;
    //newman3 = man; //不是调用拷贝构造函数,赋值操作
    }
  • 值传递的方式给函数参数传值

    1
    2
    3
    4
    5
    6
    7
    //2. 值传递的方式给函数参数传值
    //相当于隐式转换Person p1 = p;
    void doWork(Person p1) {}
    void test02() {
    Person p; //无参构造函数
    doWork(p);
    }
  • 以值方式返回局部对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //3. 以值方式返回局部对象
    Person doWork2()
    {
    Person p1;
    cout << (int *)&p1 << endl;
    return p1;//根据局部对象p1,调用新的内存拷贝p1并返回
    }
    void test03()
    {
    Person p = doWork2();
    //返回新的p1对象并传给p,会先执行拷贝函数,当函数结束再执行析构函数
    cout << (int *)&p << endl;
    }
4.2.2.1.3 浅拷贝与深拷贝*

浅拷贝:简单的赋值拷贝操作(系统默认浅拷贝)

1
2
3
4
5
Person(const Person& p) {
cout << "拷贝构造函数!" << endl;
m_age = p.m_age;
m_height = p.m_height;
}

深拷贝:在堆区重新申请空间,进行拷贝操作

1
2
3
4
5
Person(const Person& p) {
cout << "拷贝构造函数!" << endl;
m_age = p.m_age;
m_height = new int(*p.m_height);
}

区别

当出现在堆区有成员属性时,浅拷贝会导致多次调用同一个一模一样的堆的地址,当堆被释放使就会报错,而深拷贝则会将所有的都进行新的空间开辟,不会影响原数据

1
2
3
4
5
6
7
8
//析构函数
~Person() {
cout << "析构函数!" << endl;
if (m_height != NULL)
{
delete m_height;
}
}

image-20241024020916279

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

4.2.2.2 构造函数调用规则

构造函数调用规则如下:

  • 如果用户定义有参构造函数,c++不再提供默认无参构造,但是会提供默认拷贝构造

  • 如果用户定义拷贝构造函数,c++不会再提供其他构造函数

即:无参<有参<拷贝

有拷贝不提供无参与有参,有有参不提供无参提供浅拷贝,有无参默认提供空的有参和浅拷贝

4.2.2.3 初始化列表

概念:利用传入值,进行多次的类属性初始化

语法:构造函数(值1,值2):属性1(值1),属性2(值2)... {}

1
2
3
4
5
6
7
8
9
////传统方式初始化
//Person(int a, int b, int c) {
// m_A = a;
// m_B = b;
// m_C = c;
//}

//初始化列表方式初始化
Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) {}

调用传入参数:

1
Person p(1, 2, 3);

4.2.3 类成员

4.2.3.1 类对象作为类成员

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

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

函数调用顺序(此与下面[继承时的调用顺序](#4.5.4.1 构造和析构顺序)一样)

构造函数:先调用对象成员的构造,再调用本类构造

析构函数:先调用本类的析构,再调用对象成员的析构

总结:先有对象成员所以先调用对象成员构造,销毁时先销毁本类调用本类的析构,而对象成员不一定立刻就被销毁,所以后调用析构

4.2.3.2 静态成员
4.2.3.2.1 概念

静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员

4.2.3.2.2 分类

静态成员分为:

  • 静态成员变量

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

    • 在编译阶段分配内存

    • 类内声明,类外初始化

      值类型 类名::静态成员变量=值

      1
      2
      3
      4
      5
      6
      7
      8
      9
      class Person
      {
      public:
      static int m_A; //静态成员变量,类内声明
      private:
      static int m_B; //静态成员变量也是分访问权限的
      };
      int Person::m_A = 10;//类外初始化
      int Person::m_B = 10;//类外初始化
  • 静态成员函数

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

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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      class Person
      {
      public:
      static void func()
      {
      cout << "func调用" << endl;
      m_A = 100;
      //m_B = 100; //错误,不可以访问非静态成员变量
      }
      static int m_A; //静态成员变量
      int m_B; //
      private:
      //静态成员函数也是分访问权限的
      static void func2()
      {
      cout << "func2调用" << endl;
      }
      };
      int Person::m_A = 10;

访问方式:

  • 静态成员变量

    • 1、通过对象

      1
      2
      3
      Person p1;
      p1.m_A = 100;
      cout << "p1.m_A = " << p1.m_A << endl;
    • 2、通过类名

      1
      cout << "m_A = " << Person::m_A << endl;
  • 静态成员函数

    • 1、通过对象

      1
      2
      Person p1;
      p1.func();
    • 2、通过类名

      1
      Person::func();

总结:所有static的都是共享内存,静态只能访问静态

4.2.3.3 类成员变量与成员函数
4.2.3.3.1 存储方式

在C++中,类内的成员变量和成员函数分开存储

只有非静态成员变量才属于类的对象上

  • 静态成员变量与静态成员函数不占对象空间,占用全局空间
  • 类函数不占对象空间,所有函数共享一个函数实例
  • 非静态成员变量占对象空间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
public:
Person() {
mA = 0;
}
//非静态成员变量占对象空间
int mA;
//静态成员变量不占对象空间,占用全局空间
static int mB;
//函数也不占对象空间,所有函数共享一个函数实例
void func() {
cout << "mA:" << this->mA << endl;
}
//静态成员函数也不占对象空间
static void sfunc() {
}
};
4.2.3.3.2 this指针

概念:this指针指向被调用的成员函数所属的对象

this指针是隐含每一个非静态成员函数内的一种指针

this指针不需要定义,直接使用即可

用途:

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Person
    {
    public:
    Person(int age)
    {
    //当形参和成员变量同名时,可用this指针来区分
    this->age = age;
    }
    int age;
    };
  • 在类的非静态成员函数中返回对象本身,可使用return *this

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Person
    {
    public:
    Person& PersonAddPerson(Person p)
    {
    this->age += p.age;
    //返回对象本身,这样可以进行链式传递
    return *this;
    }
    int age;
    };

    void test()
    {
    Person p(10);
    p.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1);
    cout << "p2.age = " << p.age << endl;
    }
4.2.3.4 空指针访问成员函数

空指针可以访问成员函数,但只能访问没有属性或者其他成员参与的函数,因为空指针无法指定属性与成员

1
2
3
4
5
Person * p = NULL;//定义空指针对象

void ShowPerson() {
cout << this->Age << endl;//类似这样的函数,存在需要调用当前对象的属性值的时候则不能用空指针调用,空指针指向虚无,没有属性
}
4.2.3.5 常函数与常对象(const修饰)

常函数:

  • 成员函数后加const后我们称为这个函数为常函数
  • 常函数内不可以修改成员属性
  • 成员属性声明时加关键字mutable后,在常函数中依然可以修改

常对象:

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

常变量:

  • 在变量前面加const表示变量无法修改
  • 在传入函数参数中加入const,保证调用函数后不改变实参

当出现mutable修饰变量时,常对象就可以修改该成员变量,否则只能访问无法修改

实例:

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
class Person {
public:
Person() {
m_A = 0;
m_B = 0;
}

//this指针的本质是一个指针常量,指针的指向不可修改
//如果想让指针指向的值也不可以修改,需要声明常函数
void ShowPerson() const {
//const Type* const pointer;
//this = NULL; //不能修改指针的指向 Person* const this;
//this->mA = 100; //但是this指针指向的对象的数据是可以修改的

//const修饰成员函数,表示指针指向的内存空间的数据不能修改,除了mutable修饰的变量
this->m_B = 100;
}

void MyFunc() const {
//mA = 10000;
}

public:
int m_A;
mutable int m_B; //可修改 可变的
};


//const修饰对象 常对象
void test01() {

const Person person; //常量对象
cout << person.m_A << endl;
//person.mA = 100; //常对象不能修改成员变量的值,但是可以访问
person.m_B = 100; //但是常对象可以修改mutable修饰成员变量

//常对象访问成员函数
person.MyFunc(); //常对象不能调用const的函数

}

int main() {

test01();

system("pause");

return 0;
}

4.3 友元

概念:让一个函数或者类,通过关键字friend访问另一个类中私有成员

友元实现方式

  • 全局函数做友元

    friend 一个全局函数

    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
    class Building
    {
    //告诉编译器 goodGay全局函数 是 Building类的好朋友,可以访问类中的私有内容
    friend void goodGay(Building * building);

    public:

    Building()
    {
    this->m_SittingRoom = "客厅";
    this->m_BedRoom = "卧室";
    }


    public:
    string m_SittingRoom; //客厅

    private:
    string m_BedRoom; //卧室
    };


    void goodGay(Building * building)
    {
    cout << "好基友正在访问: " << building->m_SittingRoom << endl;
    cout << "好基友正在访问: " << building->m_BedRoom << endl;
    }

    void test01()
    {
    Building b;
    goodGay(&b);
    }

    int main(){

    test01();

    system("pause");
    return 0;
    }
  • 类做友元

    friend 一个类

    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
    class Building;
    class goodGay
    {
    public:

    goodGay();
    void visit();

    private:
    Building *building;
    };


    class Building
    {
    //告诉编译器 goodGay类是Building类的好朋友,可以访问到Building类中私有内容
    friend class goodGay;

    public:
    Building();

    public:
    string m_SittingRoom; //客厅
    private:
    string m_BedRoom;//卧室
    };

    Building::Building()
    {
    this->m_SittingRoom = "客厅";
    this->m_BedRoom = "卧室";
    }

    goodGay::goodGay()
    {
    building = new Building;
    }

    void goodGay::visit()
    {
    cout << "好基友正在访问" << building->m_SittingRoom << endl;
    cout << "好基友正在访问" << building->m_BedRoom << endl;
    }

    void test01()
    {
    goodGay gg;
    gg.visit();

    }

    int main(){

    test01();

    system("pause");
    return 0;
    }
  • 成员函数做友元

    friend 类名::成员函数()

    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
    63
    64
    65
    class Building;
    class goodGay
    {
    public:

    goodGay();
    void visit(); //只让visit函数作为Building的好朋友,可以发访问Building中私有内容
    void visit2();

    private:
    Building *building;
    };


    class Building
    {
    //告诉编译器 goodGay类中的visit成员函数 是Building好朋友,可以访问私有内容
    friend void goodGay::visit();

    public:
    Building();

    public:
    string m_SittingRoom; //客厅
    private:
    string m_BedRoom;//卧室
    };

    Building::Building()
    {
    this->m_SittingRoom = "客厅";
    this->m_BedRoom = "卧室";
    }

    goodGay::goodGay()
    {
    building = new Building;
    }

    void goodGay::visit()
    {
    cout << "好基友正在访问" << building->m_SittingRoom << endl;
    cout << "好基友正在访问" << building->m_BedRoom << endl;
    }

    void goodGay::visit2()
    {
    cout << "好基友正在访问" << building->m_SittingRoom << endl;
    //cout << "好基友正在访问" << building->m_BedRoom << endl;
    }

    void test01()
    {
    goodGay gg;
    gg.visit();

    }

    int main(){

    test01();

    system("pause");
    return 0;
    }

4.4 运算符重载

概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型

注意事项:运算符重载后的函数,可以被继续进行函数的重载

关键词:operator需要重载的符号(例:operator+)

4.4.1 加号运算符重载

  • 成员函数实现

    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() {};
    Person(int a, int b)
    {
    this->m_A = a;
    this->m_B = b;
    }
    //成员函数实现 + 号运算符重载
    Person operator+(const Person& p) {
    Person temp;
    temp.m_A = this->m_A + p.m_A;
    temp.m_B = this->m_B + p.m_B;
    return temp;
    }


    public:
    int m_A;
    int m_B;
    };
  • 全局函数实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //全局函数实现 + 号运算符重载
    Person operator+(const Person& p1, const Person& p2) {
    Person temp(0, 0);
    temp.m_A = p1.m_A + p2.m_A;
    temp.m_B = p1.m_B + p2.m_B;
    return temp;
    }
    //运算符重载 可以发生函数重载
    Person operator+(const Person& p2, int val)
    {
    Person temp;
    temp.m_A = p2.m_A + val;
    temp.m_B = p2.m_B + val;
    return temp;
    }
  • 调用方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Person p1(10, 10);
    Person p2(20, 20);

    //成员函数方式
    Person p3 = p2 + p1; //相当于 p2.operaor+(p1)
    cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl;

    Person p4 = p3 + 10; //相当于 operator+(p3,10)
    cout << "mA:" << p4.m_A << " mB:" << p4.m_B << endl;

总结1:对于内置的数据类型的表达式的的运算符是不可能改变的

总结2:不要滥用运算符重载

4.4.2 左移运算符重载

由于运算符<<涉及到输出流,所以会需要用到ostream

仅全局函数可以实现,配合友元可以实现输出自定义数据类型

  • 运算符重载函数
1
2
3
4
5
6
7
//全局函数实现左移重载
//ostream对象只能有一个
ostream& operator<<(ostream& cout, Person& p) {
cout << "a:" << p.m_A << " b:" << p.m_B;
return cout;//cout是ostream中的一个对象,这里的cout可以换为其他任意值,它只是表示一个输出对象而已,不是说就是针对cout对象,因为到时候会传入cout
}
//此处函数返回加引用&,是为了返回一个输出对象,而不是一个值

image-20241025003734683

  • 配合友元,实现链式调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person {
friend ostream& operator<<(ostream& out, Person& p);

public:

Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}

//成员函数 实现不了 p << cout 不是我们想要的效果
//void operator<<(Person& p){
//}

private:
int m_A;
int m_B;
};
  • 调用方式
1
2
3
Person p1(10, 20);

cout << p1 << "hello world" << endl; //链式编程

4.4.3 递增运算符重载

以下案例是自己尝试构造原本的++的作用,比如左++和右++的真正原理

可以在成员函数里面实现重载

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
class MyInteger {

friend ostream& operator<<(ostream& out, MyInteger myint);

public:
MyInteger() {
m_Num = 0;
}
//前置++
MyInteger& operator++() {
//先++
m_Num++;
//再返回
return *this;
}

//后置++
MyInteger operator++(int) {
//先返回
MyInteger temp = *this; //记录当前本身的值,然后让本身的值加1,但是返回的是以前的值,达到先返回后++;
m_Num++;
return temp;
}

private:
int m_Num;
};


ostream& operator<<(ostream& out, MyInteger myint) {
out << myint.m_Num;
return out;
}


//前置++ 先++ 再返回
void test01() {
MyInteger myInt;
cout << ++myInt << endl;
cout << myInt << endl;
}

//后置++ 先返回 再++
void test02() {

MyInteger myInt;
cout << myInt++ << endl;
cout << myInt << endl;
}

int main() {

test01();
//test02();

system("pause");

return 0;
}

4.4.4 赋值运算符重载

使用情形:当类中有属性值指向堆区,同时又出现了对象拷贝的情况,就需要用到赋值运算符重载,将系统默认的=的浅拷贝修改为深拷贝,同时为了实现链式编程(a=b=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
class Person
{
public:

Person(int age)
{
//将年龄数据开辟到堆区
m_Age = new int(age);
}

//重载赋值运算符
Person& operator=(Person &p)
{
if (m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
//编译器提供的代码是浅拷贝
//m_Age = p.m_Age;

//提供深拷贝 解决浅拷贝的问题
m_Age = new int(*p.m_Age);

//返回自身
return *this;
}

~Person()
{
if (m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
}

//年龄的指针
int *m_Age;

};

调用方法

1
2
3
4
5
6
7
Person p1(18);

Person p2(20);

Person p3(30);

p3 = p2 = p1; //赋值操作,链式编程

4.4.5 关系运算符重载

使用情形:当两个类对象需要进行比较运算的时候,需要重载关系运算符,才能实现对象直接的比较

1
2
3
4
5
6
7
8
9
10
11
bool operator==(Person & p)
{
if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
{
return true;
}
else
{
return false;
}
}

4.4.6 函数调用运算符重载

  • 函数调用运算符 () 也可以重载
  • 由于重载后使用的方式非常像函数的调用,因此称为仿函数
  • 仿函数没有固定写法,非常灵活
1
2
3
4
5
6
7
8
class MyAdd
{
public:
int operator()(int v1, int v2)
{
return v1 + v2;
}
};

调用方式

对于仿函数的调用可以先定义一个类对象,传入参数并输出给变量实现仿函数调用

也可以通过匿名对象(直接调用类名而不额外定义对象)调用来实现仿函数调用

1
2
3
4
5
6
7
8
9
void test()
{
MyAdd add;
int ret = add(10, 10);
cout << "ret = " << ret << endl;

//匿名对象调用
cout << "MyAdd()(100,100) = " << MyAdd()(100, 100) << endl;
}

总结:仿函数就是在类中重载了()符号,其实施效果与函数类似,所以叫仿函数

4.5 继承

4.5.1 继承的概念

概念:根据特征相同的共性,进行父子继承,减少重复代码

实现方式:class A : public B; ,即A继承B

  • A 类称为子类 或 派生类

  • B 类称为父类 或 基类

继承的内存管理:子类继承父类所有的属性/方法,只是无法访问权限不够仅此而已

4.5.2 继承语法

继承的语法:class 子类 : 继承方式 父类 {}

继承方式一共有三种:

  • 公共继承

    class 子类 : public 父类 {}

    能访问publicprotected,且权限不变

  • 保护继承

    class 子类 : protected 父类 {}

    能访问publicprotected,但所有能访问的,权限都继承为protected

  • 私有继承

    class 子类 : private 父类 {}

    能访问publicprotected,但所有能访问的,权限都继承为private

img

总结:继承后儿子类可以访问父亲类中publicprotected的属性/方法,并且继承对应继承方法的权限,但是父类的private不能通过继承访问(友元可以)

4.5.3 继承对象模型布局

利用工具开发人员命令提示工具查看对象模型

image-20241025022704853

打开窗口并定位到当前cpp文件目录

image-20241025022743440

定位到后,输入

1
cl /d1 reportSingleClassLayout

查看的类名 所属文件名

1545882158050

4.5.4 继承中的函数与成员

4.5.4.1 构造和析构顺序

继承中先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反

其实就是与类对象作为类成员时调用的先后一样,具体可以回去看[这里](#4.2.3.1 类对象作为类成员)

函数调用顺序

构造函数:先调用继承中父类的构造,再调用本类构造

析构函数:先调用本类的析构,再调用继承中父类的析构

总结:现有继承中父类,所以先调用继承中父类的构造,销毁时先销毁本类调用本类的析构,而继承中父类不一定立刻就被销毁,所以后调用析构

4.5.4.2 同名处理方式

成员属性(包含静态的情况)

当父类与子类中有相同名字的成员属性,就需要特殊手法进行不同的访问

在全局区或者其他非本类区:

  • 访问子类同名成员属性 直接访问即可(默认访问子类)

  • 访问父类同名成员属性 需要加作用域(类对象.父类::同名成员属性

    1
    2
    3
    4
    5
    cout << "Son  下 m_A = " << s.m_A << endl;
    cout << "Base下的m_A = " << s.Base::m_A << endl;//通过对象访问

    cout << "Son 下 m_A = " << Son::m_A << endl;
    cout << "Base 下 m_A = " << Son::Base::m_A << endl;//通过类名访问

成员函数(包含静态的情况)

这里没有考虑传入的参数是父类对象,当考虑时就是动态多态的内容了,这里先不展开

当父类与子类中有相同名字的成员函数,就需要特殊手法进行不同的访问

在全局区或者其他非本类区:

  • 访问子类同名成员函数 直接访问即可(默认访问子类)

  • 访问父类同名成员函数 需要加作用域(类对象.父类::同名成员函数

    1
    2
    3
    4
    5
    s.func();
    s.Base::func();//通过对象访问

    Son::func();
    Son::Base::func();//通过类名访问

特殊情况

如果子类中出现同名,子类会隐藏掉父类中所有同名成员函数即使重载全部失效,只要访问父类就必须要加作用域访问

这种情况是以为父类中重载了就可以直接访问了,实则不然,需要加作用域访问

4.5.6 多继承

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

注意事项:多继承可能会引发父类中有同名成员出现,需要加作用域区分

C++实际开发中不建议用多继承

4.5.7 菱形继承

菱形继承概念:

​ 两个派生类继承同一个基类

​ 又有某个类同时继承者两个派生类

​ 这种继承被称为菱形继承,或者钻石继承

菱形继承如图所示:

image-20241025025624258

缺陷:

菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义

解决方法:

利用虚继承可以解决菱形继承问题

继承前加virtual关键字后,变为虚继承

虚继承会给相同的属性/方法一个虚拟的指针,存储了偏移量

image-20241025030613388

1
2
3
4
5
//继承前加virtual关键字后,变为虚继承
//此时公共的父类Animal称为虚基类
class Sheep : virtual public Animal {};
class Tuo : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {};

4.6 多态

概念:一个属性/方法/类可以有多个形态,一般通过重载实现多态静态多态,通过派生类和虚函数实现运行时的动态多态

4.6.1 多态分类

多态分为两类:

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

静态多态和动态多态区别:

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

4.6.2 静态多态

静态多态的函数地址早绑定 - 编译阶段确定函数地址

实现方式:运用重载实现静态多态

[函数重载](#3.2 函数重载)

[运算符重载](#4.4 运算符重载)

总结:在程序运行之前就做好了多个形态的准备,能以不同的方式访问不同的形态

4.6.3 动态多态

4.6.3.1 基本概念

动态多态的函数地址晚绑定 - 运行阶段确定函数地址

实现方式:运用重写虚函数实现动态多态

重写:需要函数名与参数列表个数类型完全相同

虚函数:利用关键字virtual,对父类中的函数进行虚化,使其在运行阶段通过判断传入的参数的类型,找到对应重写的函数并执行

使用条件:在写函数时,形参需引用父类对象,在函数中调用父类中的虚函数,实现通过判断传入的参数的类型,找到对应重写的函数并执行(父类指针或引用指向子类对象)

满足条件:(满足后才能用动态多态)

  • 有继承关系
  • 子类重写父类中的虚函数
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
class Animal
{
public:
//Speak函数就是虚函数
//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。
virtual void speak()
{
cout << "动物在说话" << endl;
}
};

class Cat :public Animal
{
public:
void speak()
{
cout << "小猫在说话" << endl;
}
};

void DoSpeak(Animal & animal)
{
animal.speak();
}

void test()
{
Cat cat;
DoSpeak(cat);
}
4.6.3.2 纯虚函数与抽象类
4.6.3.2.1 纯虚函数

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,所以这种虚函数就叫做纯虚函数(正常的虚函数是帮助遇到菱形继承防止子类继承两份相同的数据,导致资源浪费以及毫无意义)

语法:

virtual 返回值类型 函数名 (参数列表)= 0 ;

4.6.3.2.2 抽象类

当当类中有了纯虚函数,这个类也称为抽象类

抽象类特点

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

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

虚析构和纯虚析构共性:(继承的时候,子类复写时可以不加virtual,但最好加,也是提醒自己,这是一个虚函数)

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

虚析构和纯虚析构区别:

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象
  • 二者只能同时存在一个

虚析构语法:

virtual ~类名(){}

纯虚析构语法:

virtual ~类名() = 0;

类名::~类名(){}

4.7 面向对象三大特征总结

封装

封装的意义:

  • 将属性和行为作为一个整体,表现生活中的事物
  • 将属性和行为加以权限控制

继承

继承的意义:

  • 根据特征相同的共性,进行父子继承,减少重复代码

多态

多态的意义:

  • 使一个函数或者属性或者类得到多次的使用
  • 使对应调用的子类对象被调用为父类指针,实现继承后依旧能实现子类对象函数

5 文件操作

C++中对文件操作需要包含头文件<fstream>

5.1 文件操作分类

操作文件的三大类:

  1. ofstream:写操作
  2. ifstream: 读操作
  3. fstream : 读写操作(通用)

5.2 文件打开方式

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

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

例如:用二进制方式写文件 ios::binary | ios:: out

5.3 文件操作步骤

  1. 包含头文件

    1
    #include <fstream>//只要涉及到文件就必须有这一步
  2. 创建流对象

    1
    2
    ofstream ofs;//这是写操作对象
    ifstream ifs;//这是读操作对象
  3. 打开文件

    1
    2
    3
    4
    5
    6
    7
    8
    ofs.open("文件路径",打开方式);

    //测试文件是否顺利打开
    if (!ofs.is_open())
    {
    cout << "文件打开失败" << endl;
    return;
    }
  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
    //普通文本写入操作
    ofs << "写入的数据";

    //二进制文件写入操作
    ofs.write((const char *)&p, sizeof(p));
    //函数原型 :ostream& write(const char * buffer,int len);
    //参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数

    //-------------------------------------------

    //二进制文件读取操作
    ifs.read((char *)&p, sizeof(p));
    //函数原型:istream& read(char *buffer,int len);
    //参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数

    //普通文本文件读取操作
    //第一种方式
    char buf[1024] = { 0 };
    while (ifs >> buf)
    {
    cout << buf << endl;
    }

    //第二种
    char buf[1024] = { 0 };
    while (ifs.getline(buf,sizeof(buf)))
    {
    cout << buf << endl;
    }

    //第三种
    string buf;
    while (getline(ifs, buf))
    {
    cout << buf << endl;
    }
    //第四种
    char c;
    while ((c = ifs.get()) != EOF)
    {
    cout << c;
    }
  5. 关闭文件

    1
    ofs.close();

核心编程部分引入

这里是黑马程序员的免费资料,本人从网盘中下载下来了,要学习完整C++基础资料的可以访问下面链接,链接为我的博客(搬运过来了),里面有C++基础资料的网盘分享,需要源文件的朋友可以去下载(在文末)

C++核心编程
  • Title: C++核心编程部分总结
  • Author: ZJ
  • Created at : 2024-10-23 12:00:00
  • Updated at : 2025-01-17 01:36:18
  • Link: https://blog.overlordzj.cn/2024/10/23/C++/核心部分总结/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments
On this page
C++核心编程部分总结