前言

b站C++课程学习笔记整理。

b站视频: 黑马程序员匠心之作|C++教程从0到1入门编程,学习编程不再难

84-88. 内存四区和new的用法

C++程序在执行时,将内存分为四个区:

  • 编译后:
    • 代码区:存放函数体的二进制代码,由操作系统进行管理。
    • 全局区:存放全局变量和静态变量以及全局常量(常量由const修饰)。
  • 程序运行后:
    • 栈区:由编译器自动分配释放,存放函数的参数值,局部变量等。
    • 堆区:由程序员分配和释放,若程序员不释放,程序运行后自动释放。

总结:

  1. 局部的在栈区,全局的在全局区。
  2. 和Java类似。只不过Java没有指针的功能。
  3. 不要返回局部变量的地址,因为局部的变量在函数执行完后已经被编译器释放了(但是我VS2022没报错并成功了不知道为啥)。
  4. 可以使用new关键字开辟堆空间(类似于Java)。这个可以返回地址。
  5. 可以使用delete关键字释放堆空间。

感觉是新版的编译器对这个地方做了很多的优化。

int* p = new int(10);注意new出来的是返回的地址,要用指针接收。

89- 90. 引用

  • 作用:给变量起nickname

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

  • 例如int &b = a(变量a的nickname是变量b。注意这里&不是取址符的作用,只是一个标记)。因为是地址,当把b的值改变时,a的值也会改变。

  • 注意事项:

    1. 引用必须初始化。比如,不能先声明:int &b;

    2. 引用初始化后就不可以改变了。比如,int &b = a后,不能再~~int &b = c~~了。

91. 引用做函数参数

作用:函数传递时,可以利用引用的技术让形参修饰实参。

优点:可以简化地址传递修改实参。

其实引用的nickname也是指针。和指针的区别是他可以直接用,不需要解引用*p。

示例代码

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
#include<iostream>;
using namespace std;

void swap1(int* p_a,int* p_b) {
int temp = *p_a;
*p_a = *p_b;
*p_b = temp;
}
// c的nickname是c,d的nickname是d。
void swap2(int &c,int &d) {
int temp = c;
c = d;
d = temp;
}

int main() {
int a = 10;
int b = 20;

cout << "调用前a是" << a << "\tb是" << b << endl;
swap1(&a, &b);
cout << "调用后a是" << a << "\tb是" << b << endl;

int c = 10;
int d = 20;
cout << "调用前c是" << c << "\td是" << d << endl;
swap2(c,d);
cout << "调用后c是" << c << "\td是" << d << endl;
return 0;
}

92. 引用做函数的返回值

  1. 不要返回局部变量的引用。
  2. 函数的调用可以作为左值。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<iostream>;
using namespace std;

int& printNum(int& a) {
return a;
}

int main() {
int a = 0;
printNum(a) = 20;
cout << a << endl;
return 0;
}

93. 引用的本质

本质:引用的本质在c++内部实现一个指针常量(指针指向的位置不可改,指针指向的值可以改)。

94. 常量引用

常量引用主要用来修饰形参,防止误操作。

在函数形参列表中,可以加const修饰形参,防止形参改变实参。

const int& ref = 10代表着int temp = 10; const int& ref = temp;

使用const修饰形参防止误操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<iostream>;
using namespace std;

void printNum(const int& a) {
cout <<"形参为" << a << endl;
}

int main() {
int a = 0;
printNum(a);
cout <<"实参为"<< a << endl;
return 0;
}

95. 函数默认参数

c++中函数形参可以有默认值。

注意的点

  1. 如果某个位置已经有了默认参数,那么从超过这个位置往后从左到右都必须由默认值。
  2. 如果函数声明有默认参数,函数的实现就不能有默认参数。声明和实现只能有一个有默认参数。

96. 函数占位参数

C++中函数的形参列表里可以有占位参数,用来做占位,调用时必须填补这个位置。占位参数可以有默认参数。

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

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>;
using namespace std;

void printNum(int a, int) {

}
void printNum2(int a, int = 10) {
}

int main() {
int a = 0;
printNum(a,10);
printNum2(a);
return 0;
}

97. 函数重载

就是Java中的方法重载,一样的。

98. 函数重载注意事项

  • 引用作为重载条件
  • 函数重载碰到函数默认参数

当引用作为重载条件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iostream>;
using namespace std;

void printNum(int &a) {
cout<<"不加const的函数被调用" << endl;
}
void printNum(const int &a) {
cout << "加了const的函数被调用" << endl;
}

int main() {
int a = 10;
printNum(a);

printNum(10);
return 0;
}

尽量避免函数重载碰到函数默认参数产生二义性:

函数重载二义性

99-100. 类和对象-封装-属性和行为做为整体

面向对象的三大特征:封装、继承和多态。

封装:将属性和整体作为一个整体,表现生活中的事物。

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

跟Java区别是权限符是在类里面写的。

示例:设计一个学生类,包含姓名学号get, set方法等(经典老番)。

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
#include<iostream>;
using namespace std;

class student {
public:
string s_name;
int s_age;

student() {}

student(string name, int age) {
s_name = name;
s_age = age;
}

void setName(string name) {
s_name = name;
}
void setAge(int age) {
s_age = age;
}
string getName() {
return s_name;
}
int getAge() {
return s_age;
}

void study() {
cout << "学生可以学习" << endl;
}
};

int main() {
student s1;
// 注意c++里new出来的对象要用指针接收
student* p= new student("孙健耕", 21);
s1 = *p;
cout << "年龄是" << s1.getAge() << endl;

student s2 = student("孙健耕", 22);
cout << "年龄是" << s2.getAge() << endl;
}

101. 访问权限

  • 公共权限 public 都可以用。

  • 保护权限 protected 类内可以用,类外不可以用,但是继承的子类可以用。

  • 私有权限 private 类内可以用,类外也不可以用。

102. class和struct区别

其实这俩功能很像。唯一区别:

  • struct默认是public。
  • class默认是private。

103. 成员属性设置为私有

优点:可以自己控制读写权限。对于写权限,可以查看数据的有效性。

106. 对象的初始化和清理

初始化:构造函数。

清理对象的内容:析构函数。

编译器会默认提供一个默认的空的构造函数和空的析构函数。

  • 构造函数:类名(){}

  • 析构函数:~类名(){}。注意,析构函数不可以有参数,因此不可以方法重载。

107. 构造函数的分类和调用

两种分类方式:

  • 按参数,分为:有参构造和无参构造。
  • 按类型,分为:普通构造和拷贝构造。

三种调用方法:

  • 括号法
  • 显示法
  • 隐式转换法

注意:使用无参构造的时候不要写小括号,否则编译器会认为这是一个函数的声明。

示例代码:

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
#include <iostream>
using namespace std;

class student
{
public:
string s_name;
int s_age;

// 构造
student() {
cout << "无参方法被调用" << endl;
};
student(string name,int age) {
cout << "带参方法被调用" << endl;
};
student(string name) {
cout << "带参方法被调用" << endl;
};
// 析构
~student() {
cout << "析构函数被调用" << endl;
};

//拷贝
student(const student &s) {
s_name = s.s_name;
s_age = s.s_age;
cout << "拷贝方法被调用" << endl;
};
};


int main(){
// 括号法
student s1; //注意这里不要加括号
student s2("孙健耕", 21);
student s3(s2);
// 显示法
student s4 = student();
student s5 = student("孙健耕", 21);
student s6 = student(s5);

//注意事项1:匿名对象,创建完后系统会自动销毁。
student();
//注意事项2:不要用拷贝构造函数构造匿名对象。
//student(s6);
// 隐式转换法
//不推荐

system("pause");
return 0;
}

108. 拷贝构造函数的调用

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

  1. 使用一个已经创建完毕的对象来初始化一个新对象
  2. 值传递的方式给函数参数传递
  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
#include <iostream>
using namespace std;

class student
{
public:
string s_name;
int s_age;

// 构造
student() {
cout << "无参方法被调用" << endl;
};
student(string name,int age) {
cout << "带参方法被调用" << endl;
};
student(string name) {
cout << "带参方法被调用" << endl;
};
// 析构
~student() {
cout << "析构函数被调用" << endl;
};

//拷贝
student(const student &s) {
s_name = s.s_name;
s_age = s.s_age;
cout << "拷贝方法被调用" << endl;
};
};
student doWork(student s) {
return s;
};

int main(){

student s1;
student s2(s1);
doWork(s1);

system("pause");
return 0;
}

109. 构造函数的调用规则

默认情况下,编译器至少给一个类添加三个函数

  1. 默认构造函数(无参,函数体为空)。
  2. 默认析构函数(无参,函数体为空)。
  3. 默认拷贝构造函数,对属性进行值拷贝。
  • 和Java一样,如果用户定义有参构造,c++不会提供无参构造,但是会提供一个拷贝。
  • 如果用户定义拷贝构造,那C++不会提供任何别的默认构造方式了。

110. 深拷贝与浅拷贝

  • 浅拷贝:简单的复制拷贝操作。

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

  • 编译器默认提供的是浅拷贝。如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,解决浅拷贝的问题。

  • 析构函数的作用:可以在析构函数中人为的清除堆区的属性。

浅拷贝的错误(属性在堆区开辟)

解决:使用自定义的拷贝函数进行深拷贝。

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
#include <iostream>
using namespace std;

class student {
public:
string s_name;
int* s_age;
student() {
cout << "无参构造" << endl;
}
student(string name,int age) {
cout << "带参构造" << endl;
s_name = name;
s_age = new int(age);
}

student(const student &s) {
s_name = s.s_name;
s_age = new int(*s.s_age);
}

~student() {
cout << "析构函数" << endl;
if (s_age != NULL) {
delete s_age;
s_age = NULL;
}
}
};
void test() {
student s1("孙健耕", 21);
student s2(s1);
}
int main() {

test();
system("pause");
return 0;
}

111. 初始化列表

作用:

C++提供了初始化列表的语法,用来初始化属性。

语法:构造函数(): 属性1(值1),属性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
#include <iostream>
using namespace std;

class student {
public:
string s_name;
int s_age;

student(string name, int age) :s_name(name), s_age(age) {
cout << "初始化列表构造方法被调用" << endl;
}

};
void test() {
student s1("孙健耕", 21);
cout << s1.s_age << s1.s_name << endl;
}
int main() {

test();

system("pause");
return 0;
}

112. 类对象作为类成员

C++类中的成员可以实另一个类的对象。

例如:

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

当创建B对象时,A与B的构造与析构的顺序是?

构造:先创建一个A类对象,再来构造B类。

析构:先析构B类,再析构A类。

113. 静态成员

分为静态成员变量和静态成员函数。

  • 静态成员变量
    • 所有对象共享同一份数据
    • 再编译阶段分享内存
    • 类内声明,类外初始化

比如:动态的是不同的对象之间可能不同的数据。静态的是不同的对象间肯定相同的数据。const是不同对象间肯定相同,且无法修改的数据。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

class student {
public:
static string s_name;
};
string student::s_name = "Kalle";
void test() {
student s1;
cout << s1.s_name << endl;
student s2;
s2.s_name = "Jack";
// 两种打印方式
cout << s1.s_name << endl;
cout << student::s_name << endl;
}
int main() {

test();
system("pause");
return 0;
}
  • 静态成员函数
    • 所有对象共享同一个函数
    • 静态成员函数只能访问静态成员变量

为什么静态成员函数只能访问静态成员变量?因为静态成员函数是共享的,当调用时,如果访问动态成员变量,那他就不知道该访问哪个对象的动态成员变量了。

示例代码:

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
#include <iostream>
using namespace std;

class student {
public:
static string s_name;
static void dowork() {
s_name = "Jack";
}
};
string student::s_name = "Kalle";
void test() {
student s1;
cout << s1.s_name << endl;

// 两种访问方式
//s1.dowork();
//cout << s1.s_name << endl;
student::dowork();
cout << s1.s_name << endl;


}
int main() {

test();
system("pause");
return 0;
}

注意,静态成员方法是可以有访问权限要求的。

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

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

  • 空对象(类是空的)占一个字节(目的是区分不同的空对象)。
  • 非对象中:
    • 成员变量按类型占相应的字节。(比如一个int占4个,两个int成员变量占8个)
    • 成员方法、以及静态的变量和方法都不属于类的对象上。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

class student {
public:
int name;
int age;
};

void test() {
student s1;
cout << sizeof(s1) << endl; //8
}
int main() {

test();
system("pause");
return 0;
}

115. this指针的用途

  • 解决变量重名(和Java一样)。不同的是C++里this是指针,指向的是调用这个指针的对象。所以应该用指针的方式去使用this。例如:this->name = name
  • 利用解引用的方式返回本体。 例如:return *this;

示例代码:

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
#include <iostream>
using namespace std;

class student {
public:
int age;

student(int age) {
this->age = age;
}

student& addAge(student &s) {
this->age += s.age;
return *this;
}
};

void test() {
student s1(10);
cout << s1.age << endl;
}
void test2() {
student s2(10);
student s3(10);
s2.addAge(s3).addAge(s3).addAge(s3).addAge(s3).addAge(s3);
cout << s2.age << endl;
}
int main() {

test2();
system("pause");
return 0;
}

注意在返回自身的引用的函数addAge中,

  • 参数使用引用的方式,是因为这样做不需要调用系统的默认浅拷贝函数,直接通过引用(本质是个指针),进行地址操作,节省空间。
  • 返回值使用引用的方式,是因为这样做不需要调用系统的默认浅拷贝函数。否则就会返回一个拷贝后的新的student,虽然和s2是一模一样,但是他们本质上是两个对象。这样的返回值是20,而不是正确的60。

116. 空指针访问成员函数

C++中空指针也是可以调用成员函数的,但是要注意有没有用到this指针。如果用到了this指针,就要判断一下,保证代码的健壮性。

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
#include <iostream>
using namespace std;

class student {
public:
int age;

student(int age) {
this->age = age;
}

void showAge() {
cout << "成员函数被调用" << endl;
}
void showAge1() {
if (this == NULL) {
return;
}
// 这里的age编译器默认是this->age
cout << age << endl;
}
};

void test() {
student* p = NULL;
p->showAge();
p->showAge1();
}


int main() {

test();
system("pause");
return 0;
}

117. const修饰成员函数

常函数:

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

常对象:

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

示例代码:

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
#include <iostream>
using namespace std;

class student {
public:
int age;
mutable int number;

student(int age,int number) {
this->age = age;
this->number = number;
}

void showAge() const{
//age = 10;
//this->age = 10;
number = 20;
this->number = 30;
}
void showAge1() {

}
};

void test() {
const student s = student(10,20);
s.showAge();
s.number = 20;
//s.age = 20;
// s.showAge1();

}


int main() {

test();
system("pause");
return 0;
}

118. 友元

在程序里,有些私有属性,也想让类外特殊的一些函数或者类进行访问,这时就需要友元技术。

关键字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
#include <iostream>
using namespace std;

class building {
friend void myFriend(building& b);
public:
string sittingroom;
private:
string bedroom;
public:
building() {
sittingroom = "客厅";
bedroom = "卧室";
}
};

void myFriend(building &b) {
cout << "正在访问:" << b.sittingroom<<endl;
cout << "正在访问:" << b.bedroom << endl;

}
int main() {
building b = building();
myFriend(b);
system("pause");
return 0;
}

119. 类做友元

让一个类可以访问另一个类中的所有的成员内容。

构造函数也可以在class外构造,通过域访问。

示例代码:

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
#include <iostream>
using namespace std;

// Building类要写在前面,如果不想写在前面,要先声明
class Building;
class GoodFriend {
public:
// 类外构造,类内必须提前声明
GoodFriend();
Building* building;
void visit();
};

class Building {
friend class GoodFriend;
public:
// 类外构造,类内必须提前声明
Building();
private:
string bedroom;
public:
string sittingroom;
};


Building::Building() {
bedroom = "卧室";
sittingroom = "客厅";
}

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

void GoodFriend::visit() {
cout << "好兄弟正在访问:" << building->sittingroom << endl;
cout << "好兄弟正在访问:" << building->bedroom << endl;
}

int main() {
GoodFriend f = GoodFriend();
f.visit();
system("pause");
return 0;
}

120. 成员函数做友元

不是一整个类,而是让某个类中的某个成员函数可以做友元:

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
#include <iostream>
using namespace std;

// Building类要写在前面,如果不想写在前面,要先声明
class Building;
class GoodFriend {
public:
// 类外构造,类内必须提前声明
GoodFriend();
Building* building;
void visit();
};

class Building {
friend void GoodFriend::visit();
public:
// 类外构造,类内必须提前声明
Building();
private:
string bedroom;
public:
string sittingroom;
};


Building::Building() {
bedroom = "卧室";
sittingroom = "客厅";
}

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

void GoodFriend::visit() {
cout << "好兄弟正在访问:" << building->sittingroom << endl;
cout << "好兄弟正在访问:" << building->bedroom << endl;
}

int main() {
GoodFriend f = GoodFriend();
f.visit();
system("pause");
return 0;
}

121. 加号运算符重载

示例代码:

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
#include <iostream>
using namespace std;

class person {
public:
int num;
int age;

person() {
};

person(int num, int age) {
this->age = age;
this->num = num;
};

person operator+ (person& p) {
person temp;
temp.age = this->age + p.age;
temp.num = this->num + p.num;
return temp;
}
};
// 类外定义
/*
person operator+(person &p1,person &p2) {
person temp;
temp.age = p1.age + p2.age;
temp.num = p1.num + p2.num;
return temp;
}
\*/

void test() {
person p1 = person(10, 20);
person p2 = person(30, 40);

person p3 = p1 + p2;
cout << "age是" << p3.age << "num是" << p3.num << endl;
}
int main() {
test();
system("pause");
return 0;
}

有类内和类外两种定义方式。

  • 有点类似于Java里的方法重写。感觉C++里对重载和重写定义不是很明白。

  • 对于内置的数据类型的表达式的运算符是不可以改变的

  • 不要滥用