大一学习 c++ 的时候只是简单的学习了面向对象编程,了解了相关的知识点,但是很多实用技巧、要点都没有了解到。偶然间翻到侯捷大佬的 C++ 大系视频,看了两三集,知道了好多东西。于是决定把系列刷完,同时辅以网上的一些资料,做一个整理笔记
此系列持续更新
面向对象编程
头文件与类
整体工程如下:
1 | bin |
- 头文件包含函数和类的声明,以及用到的标准库文件;函数和类的定义写入源文件,需要 include 头文件
- 模板类在头文件中声明及定义
原因是类可以由编译器直接实例化,定义在源文件可以进行检查;模板类由于不知道类型,无法检查,直接写入头文件最好
- 头文件一定要写
#define
保护,防止被多重包含命名规则:CMakeList 中的项目工程名(大写)_类名(大写)_H1
2
3
4
5
6
7
// 函数及类的声明
// 模板函数及模板类的声明和定义
例子:EXAMPLE_TEST_H
- 函数名称首字母小写,第二个单词首字母大写,如
getValue
;类首字母大写,第二个单词首字母大写。如IntTest
- 定义成员函数时尽可能
inline
类中直接定义的函数不需要
inline
修饰类中声明的成员函数不需要用
inline
修饰,在类外定义时需要inline
修饰inline
只是对编译器的建议,能不能内联要看编译器让不让,会自行决定
构造函数
1 | // Test.h |
- 默认构造函数在声明中直接 default
- 构造函数中尽量使用初始化列表
初始化列表不会进入构造函数的函数体,相当于少一层赋值的步骤,提高了效率
- 初始化成员变量时,按照声明顺序,而不是按照构造函数的顺序;所以初始化列表的顺序按照声明顺序构造
拷贝构造与拷贝赋值
为什么有的类需要写拷贝构造函数,有的类不需要写
有的类成员变量就是简简单单的值,编译器会自动写拷贝构造函数用来拷贝构造对象;有的类成员变量是一个指针,例如 string 类,指针指向字符数组,如果也是使用编译器写的拷贝构造函数,新对象的指针记录的是原来对象指针记录的内存地址,即两个对象指针指向同一个字符数组,那就不是拷贝了,不符合建立拷贝对象时的想法
浅拷贝:拷贝的对象中的指针(成员变量)指向被拷贝对象中的指针指向的内存地址
深拷贝:拷贝的对象中的指针指向一块新建立的内存地址,这个内存地址的内容完全复制被拷贝对象中的指针指向的内容
当一个类的成员变量有指针时,多半要写拷贝构造和拷贝赋值
拷贝构造函数的参数应该是一个
const
修饰的同类的对象的引用1.
const
修饰:防止被拷贝的对象被修改
2. 引用:传递的时候速度快,效率高
1 | inline String::String(const String &str) { |
拷贝赋值函数
- 检测是否自我赋值,是的话直接把 this 传回去
- delete 内存
- new 新的内存地址
- 拷贝
- 返回
1 | inline String &String::operator=(const String &str) { |
为什么要自我检测?
如果没有自我检测,当确实发生了传入自己的时候(
str=str
),由于先delete
了内存空间,接下来要检测长度和拷贝的时候就傻眼了,啥也没了。因为我把自己传进来并且先 delete 掉了我杀我自己 👋
析构函数
- 当成员变量是指针且 new 了动态内存时,需要用析构函数 delete
- 指针指向数组时,
delete[] this->ptr
内存管理
- 栈(stack):程序运行时自动分配释放 ,存放函数的参数值,局部变量的值等
- 堆(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由 OS 回收
- 全局区(也叫静态区):存放全局变量和静态变量,以上变量作用域结束后仍然存在,程序结束后由系统销毁
- String str(“hello”):建立在栈中,在作用域结束时自动调用析构函数结束自己
- String str = new String:建立在堆中,由程序员自己控制建立和销毁,需要自己
delete
;如果忘记了delete
就会内存泄漏
6.new
时干了啥(String str = new String(“hello”))- 分配内存:调用 C 的 malloc(sizeof(String)),得到一个指向内存地址的指针(void)
- 转型:把 void 指针转型为需要的指针,并赋值为一开始建立的指针(
str
) - 调用构造函数:str—>String::String(“hello”),这时候成员变量的指针 new 了一个内存空间
delete
时干了啥(delete str)- 调用析构函数:String::~String(str)
- 释放内存:调用 C 的 free(str)
首先明确一下概念,str 这个指针指向一个内存地址,这个地址保存了一个对象;这个对象里有一个成员变量,是一个指针,这个指针指向了一个 new 出来的内存空间
调用析构函数时,这个对象 delete 了自己的成员变量指向的内存空间
释放内存时,释放了这个 str 指针持有的内存空间(这个空间保存了这个对象)
- 用了
new []
就要用delete[]
在 new 的时候,分配的内存块的边界会标明整个内存块有多大。然后在这个大的内存块里分出了 n 个小内存块来用。
如果用 delete[],编译器就会知道要把这 n 个小内存块先回收,再回收整个大内存块
如果用 delete,编译器只会把第一个小内存块回收,然后回收整个大内存块
好像也是一种内存泄露,但是跟我一开始学的内存泄露的概念不太一样。以上写的这块内容不确定对不对,找个时间再琢磨一下
参数传递与返回值
对于不会修改类成员变量的函数,在函数名后面加上
const
例子:
int getA() const;
假设实例化一个 const 对象,调用没有 const 的 getA()函数,会认为会修改成员变量,与这个 const 对象冲突。
参数传递少用传值,尽量传引用,不希望被传值被修改时加上
const
更好其实没必要这样。比如传一个对象进去,传值的话会传很大的数据进去,这时候传引用就相当于传指针,很小的一块数据就可以指向很大的东西;如果传个数字啥的,直接传值也没什么大不了的
返回值也可以在适当情况下返回引用
要注意是适当情况下,有时候返回成员变量的值就不必返回引用了;返回一个对象时可以返回引用
函数内部创建的局部变量返回的必须是值,不能是引用
函数结束后这个局部变量就销毁了,那返回引用就没用了
friend
利用
friend
声明函数或另一个类为友元,可以访问直接成员变量友元打破封装,有时比较方便
同一个 class 的对象互为 friend
即声明两个 Test 的对象,可以互相访问对方的成员变量
操作符重载
- 用
operator
指明操作符重载——针对成员函数
1 | Test &Test::operator+=(const Test &test) { |
- 参数中默认传入当前对象的
this
指针 - 返回值返回引用
- 针对非成员函数,创建临时对象用来返回,返回值
1 | Test operator+(const Test &test1, const Test &test2) { |
甚至存在简化方法
1 | Test operator+(const Test &test1, const Test &test2) { |
然后 CLion 又给简化了
1 | Test operator+(const Test &test1, const Test &test2) { |
我直呼 666,以前还觉得用编辑器就够了,现在发现 IDE 是真滴行 👍
因为创建了临时对象用来保存值,所以传入的两个对象需要用const
修饰,防止被修改
5. 取反操作
1 | Test operator-(const Test &test) { |
- 输出流
1 | std::ostream &operator<<(std::ostream &os, const Test &test) { |
cout 输出的是 ostream 流,所以需要定义时需要传入 ostream 引用,最后函数返回的也是一个 os 流
static
- 一般的成员函数有
this
指针,并作为默认参数传入,不要在声明和定义的时候在函数头写出来,但是在函数体里可以用;static 函数没有this
指针 - 类的 static 函数只有一个,不属于任何对象
- 调用 static 函数:可以通过类名直接调用
A::getInstance()
通过这种方式可以调用函数:1
2
3
4
5
6
7
8
9
10
11
12class A {
public:
static A &getInstance() {
static A a;
return a;
}
void setup() {}
private:
A() = default;
};A::getInstance().steup()
模板
1 | template<typename T> |
复合
- 一个类以另一个类的对象作为成员变量,调用对象的一些方法实现需要的功能
- 构造时由内至外
- 析构时由外至内
委托
- 创建指向一个类(A)的指针,另一个类(B)以这个指针作为成员变量
- 功能写入类 A,类 B 调用类 A 的功能,做到无论类 A 如何改动,都不会影响类 B,即内部是外部功能的实现,外部是内部对外的接口
复合和继承的差异
复合的外部和内部同时出现,拥有一致的生命周期
委托可以先创建外部,等需要用到内部时再创建,生命周期不一致
继承
- 创建一个类时,不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类
- 继承关系有 public,protected 和 private
- 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问
- 保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员
- 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员
- 派生类可以继承多个基类,拥有多个基类的特性
- 构造时由内至外
- 析构时由外至内
虚函数
- 数据可以被继承,函数可以被继承调用权(派生类可以调用基类的函数
- virtual 函数:基类的 virtual 函数有默认定义,派生类继承调用权后可以进行覆写(override,重新定义)
- pure virtual 函数:基类的 pure virtual 函数没有默认定义,派生类继承调用权后必须覆写
- 框架思想:基类定义一套动作,其中关键的步骤设置为 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
25class A {
public:
virtual std::string getName() = 0; // 纯虚函数
void info() {
std::cout << "输出信息" << std::endl;
std::cout << getName() << std::endl;
}
};
class B : public A {
public:
std::string getName() {
return this->name;
}
private:
std::string name;
};
int main() {
B b();
b.info();
return 0;
}对象 b 调用基类的 info()函数,执行到 getName()函数时,发现是虚函数,回到派生类 B,发现了覆写后的函数,调用覆写后的函数,再回到 info()函数执行完。
对象 b 调用基类的 info()函数时,b 作为 this 指针以隐藏参数的形式传入 info()函数