转码基本功

转码基本功

  • 前言
  • 一、虚函数与纯虚函数
    • 1.虚函数与纯虚函数的概念
    • 2.虚函数要加override
    • 3.虚析构函数
    • 4.虚函数的实现
    • 5.析构函数可以定义为虚函数,基类析构函数要定义为虚函数
    • 6.构造函数不可以定义为虚函数
    • 7.隐藏
  • 二. 内存四区
    • 2.1 程序运行前
    • 2.2 程序运行中
  • 三. Static的5中用法
  • 四. 智能指针
    • 4.1 unique_ptr
    • 4.2 shared_ptr
    • 4.3 weak_ptr
  • 五. new和delete
    • 5.1 底层原理
    • 5.2 和malloc和free的区别
    • 5.3 内存池技术
  • 六. define和const
  • 七. 定义与声明
  • 八. 初始化列表
  • 九. 数组名和指针
  • 十. struct和class的区别
  • 十一. static
    • 11.1 不在类中
    • 11.2 在类中
  • 十二. const
  • 十三. 全特化与偏特化
  • 十四. 构造函数
    • 14.1 构造函数的3种形式
    • 14.2 拷贝构造函数要传引用,不能传值
    • 14.3 拷贝构造函数被调用的3种情况
    • 14.4 单参数构造函数尽可能使用显式
  • 十五. 浅拷贝与深拷贝
  • 十六. inline函数
  • 十七. NULL和nullptr
  • 十八. 操作符重载
  • 十九. 右值引用
  • 二十. 零拷贝
  • 二十一. 多重继承与虚继承
  • 二十二. 友元


前言

例作为传统的车辆工程的学生,转码之路可谓艰辛。本章博客记录转码过程遇到的C++基本知识,作为日后复习用。


一、虚函数与纯虚函数

1.虚函数与纯虚函数的概念

虚函数是实现多态的基础。如下所示,fun1是虚函数,func2是纯虚函数。
多态是指:不同类的对象对同一消息的不同响应。
动态绑定是指:根据不同的条件将基类指针指向不同的派生类。

class base {
public:
	virtual void func1() {
		cout << "11" << endl;
	};
	virtual void func2() = 0;
};
class child {
public:
	virtual void func1() {
		cout << "22" << endl;
	};
	virtual void func2() {
		cout << "33" << endl;
	};
};

如果没有func2,基类是可以实例化的。有纯虚函数的类不可以实例化。

base *p = new base();

只有通过基类指针或引用调用虚函数时,才会发生动态绑定。

child c;
base *p = &c;  // 基类指针
base &ref = c; // 基类引用

2.虚函数要加override

在派生类重写虚函数时,要加override,表示你一定重载的是某个基类的函数。

virual void fun() override {};

3.虚析构函数

一般派生类在执行构造函数时,先执行基类,再执行子类。
若析构函数不是虚函数,则delete时,仅调用基类的虚构函数,子类的虚构函数不会调用。因此要将基类的析构函数声明为虚函数,才能先调用基类,再调用子类。

base *p = new child();
delete(p);
// 执行结果如下
// base() -> child() -> ~child() -> ~base()

4.虚函数的实现

虚函数是通过虚函数表虚表指针实现的,虚函数表示存储了该类虚函数的指针数组,其中的各个指针指向各个虚函数。每个类的实例都有一个指向这个虚函数表的指针,称为虚表指针,虚表指针指向内存中同一块虚函数表。
编译器是这样做的:基类的虚函数指针指向基类的虚函数,子类的虚函数指针根据对应的虚函数是否重写而指向子类虚函数或者基类的虚函数。
所以类的实例在调用虚函数时,是通过指针查找到虚函数的位置,然后运行它。
虚表指针是类的第一个数据成员。

5.析构函数可以定义为虚函数,基类析构函数要定义为虚函数

基类析构函数之所以要定义为虚函数,是要在如下场景中获得应用:当用父类指针指向子类时,如果父类的析构函数不是虚函数,那么仅仅调用父类的析构函数,可能造成子类的对象没有被完全释放,造成内存泄漏。

class A {
public:
  ~A() {
    cout << "123" << endl;
  }
};

class B : public A {
public:
  ~B() {
    cout << "456" << endl;
  }
};

int main() {
    A* a = new B();
    delete(a);
    return 0;
}

按照如下的方式,将基类析构函数定义为虚函数,则会先调用子类的析构函数,再调用基类的析构函数,保证所有对象得到释放。

class A {
public:
  virtual ~A() {
    cout << "123" << endl;
  }
};

class B : public A {
public:
  virtual ~B() {
    cout << "456" << endl;
  }
};

int main() {
    A* a = new B();
    delete(a);
    return 0;
}

6.构造函数不可以定义为虚函数

7.隐藏

隐藏是说子类和父类有同名函数,不管参数列表/返回值是否一致,子类的对象会调用子类的同名函数,而不是父类的同名函数,所以相当于父类的同名函数被隐藏了。
在虚函数这里写隐藏是因为隐藏虚函数都实现了类似的功能,即子类调用子类的函数,父类调用父类的函数。

二. 内存四区

内存四区我个人觉得内容不难。

2.1 程序运行前

程序运行前只用到了代码区和全局区。
代码区存放程序的机器指令。是只读的,防止修改。
全局区又名为静态区,存放全局变量、静态变量与全局常量

#include
using namespace std;
// 全局变量在全局区
int a = 1;
// 全局常量在全局区
const int b = 2;
// 全局静态变量在全局区
static int c = 3;
int main() {
  // 局部变量在栈区
	int d = 4;
	// 局部常量在栈区
	const int e = 5;
	// 局部静态变量在全局区
	static int f = 6;
	return 0;
}

2.2 程序运行中

在程序运行中,部分变量在栈区(stack)与堆区(heap)
栈区存放的是局部变量,形参。由编译器自动分配。

// a和b都是形参,在栈区
int func(int a, int b) {
	// c是局部变量,在栈区
	int c = 3;
	return a + b + c;
}

堆区存放的是由malloc或new开辟出来的变量。需要由程序员自行开辟与释放。
因此,对于一个类A,A a得到的对象a存储在栈中,A* a = new A()得到的在堆中,需要自己释放。

int main() {
	// *head是局部变量,存在栈区,栈区内容是指向堆区的地址,堆区存放着对应的量
	TreeNode* head = new TreeNode();
	// 需要由程序员自行释放
	delete(head);
	return 0;
}

三. Static的5中用法

在知乎文章《C/C++ 中的static关键字》中详细讲解了static的5种用法,分别是:

  1. 静态成员变量
    
  2. 静态成员函数
    
  3. 静态全局变量
    
  4. 静态局部变量
    
  5. 静态函数
    

静态成员变量可以由所有对象调用,静态全局变量和静态局部变量可以反复调用和使用,因为存在全局区。只要提到静态变量就是在全局区。这个知识点可以和内存四区联系起来。

静态函数和全局静态变量一样,对于其他文件是不可见的,不能调用。

具体请参见链接:
https://zhuanlan.zhihu.com/p/37439983

四. 智能指针

智能指针分类:unique_ptr, shared_ptr, weak_ptr。
底层原理:智能指针是基于类模板实现的(模板类是类模板实例化的结果),在作用域结束时自动调用析构函数删除指针,避免内存泄漏。

4.1 unique_ptr

unique pointer对对象是独占式的,在一个时刻只有一个该指针指向该对象。
原理:禁用拷贝构造函数和赋值函数。

4.2 shared_ptr

shared_ptr能够允许多个指针指向同一个对象。
原理:采用计数机制,多一个指针多一次计数,每销毁一个指针计数就少一次,技术到0的时候释放资源。
存在的问题:循环引用。在双向链表/二叉树等情况下指针较多,两个shared_ptr互相指向的话,计数为2,销毁时计数只能减到1,所以对象不会被释放。

4.3 weak_ptr

weak_ptr能够解决shared_ptr循环引用的问题,weak_ptr指向shared_ptr时,shared_ptr的计数不会增加,在该情形下,对象就能被顺利释放。

五. new和delete

5.1 底层原理

1.new是先调用malloc申请内存,然后用初始化的值调用构造函数,最后返回对象的指针;
2.delete是先调用析构函数,然后free释放内存。

5.2 和malloc和free的区别

1.malloc和free是标准库函数,new和delete是运算符;
2.new调用malloc,delete调用free。

5.3 内存池技术

内存池技术是为了避免程序频繁调用new/malloc和delete/free而产生内存碎片,提高性能而创建的一套技术。
new/malloc和delete/free是标准库函数,内存池技术是开发人员自行编程实现的,属于应用程序。
内存池的原理是提前在内存中申请一定数量,固定大小的内存块,程序调用时将内存块分配给各个对象,不用之后再回收,避免了内存碎片的产生。
内存池可以通过类实现,在类中记录申请的内存块的大小,重载new和delete。

六. define和const

1.define在预处理阶段替换;const在编译阶段确定值;
2.define在内存中可能有多份拷贝;const在内存的全局区/静态区,只有一份;
3.define不做安全性检查;const做安全性检查。

七. 定义与声明

1.定义分配内存,声明不分配内存;
2.定义包含声明,声明并不包含定义。
3.变量只能被定义一次,但是可以声明多次(extern int a)。

八. 初始化列表

初始化列表直接调用了拷贝构造函数进行初始化,不用的化是用默认构造函数+赋值。

九. 数组名和指针

1.都可以用增减偏移量来访问数组元素。以下输出2和2。

int a[3] = {1, 2, 3};
int *p = a;
cout << *(a + 1) << " " << *(p + 1) << endl;

2.数组名不能自增自减,指针可以自增自减。
3.若数组名被作为指针传入函数,则退化成一般指针,可以自增自减。

void func(int *p) {
	p++;
}
int a[3] = {1, 2, 3};
func(a);

十. struct和class的区别

1.默认的访问权限,struct是public,class是private;
2.class可以作为模板参数关键字,struct不能。

template<class T>

十一. static

11.1 不在类中

1.用static修饰的全局变量和局部变量都存在内存的全局区/静态区;
2.static修饰的全局变量在本cpp文件中使用,不能在其他文件中使用;
3.static修饰的函数在本cpp文件中使用,不能在其他文件中使用。
4.一般的函数默认是全局函数。

11.2 在类中

1.static修饰的成员变量必须在类外初始化;

type classname::member = value;
int A::a = 0;

2.static与非static函数都能调用static变量;
3.static函数只能调用static变量;
4.static与非static函数都能调用static函数,static函数不能调用非static函数。
总的来看,static能被非static调用。

十二. const

1.const在*左边,表示指向的对象不能变。

int a = 1;
const int *p1 = &a;
*p1 = 5; // 错误

2.const在*的右边,表示指针指向不能变。

int a = 1, b = 2;
int * const p1 = &a;
p1 = &b; // 错误

十三. 全特化与偏特化

对于模板函数,可以指定模板参数中的一个或者多个,如果指定所有的模板参数,则称为全特化;如果指定部分参数,则称为偏特化。
全特化举例如下:

template<typename T1, typename T2>
void func(T1 t1, T2 t2) {
	cout << "11" << t1 << t2 << endl;
}
template<>
void func(char c1, char c2) {
	cout << "22" << c1 << c2 << endl;
}

偏特化举例如下:

template<typename T3>
void func(char c1, T3 t3) {
	cout << "33" << c1 << t3 << endl;
}

十四. 构造函数

14.1 构造函数的3种形式

构造函数有如下几种形式:
1.默认构造函数。在没有定义构造函数时,编译器默认提供的构造函数。
2.重载构造函数。可以有参,可以无参。

class A {
public:
  A() = default;
  A(int a, int b) {
    cout << "111" << endl;
  }
  A(int v) {
    cout << "222" << endl;
  }
};

3.拷贝构造函数。将已经实例化的一个对象作为参数,将其复制给自己。

14.2 拷贝构造函数要传引用,不能传值

函数的形参是实参的一份拷贝,如果传递的值是一个类对象,那么需要调用拷贝构造函数进行拷贝。以下为例,a作为实参到拷贝构造的形参,需要调用拷贝构造函数,在该拷贝构造函数中,又要再调用拷贝构造函数拷贝一份形参,如此形成了循环导致出错。
引用是变量的别名,可以直接找到对象本身,所以可以直接传引用。

A (A a) { // 传值是错误的x
	...
}
...
A a;
A b(a);

14.3 拷贝构造函数被调用的3种情况

1.作为形参传入;
2.作为返回值返回;

A func(A a) {
	return a;
}

以上代码会调用两次拷贝构造函数。
3.把一个类对象拷贝给另一个。

14.4 单参数构造函数尽可能使用显式

使用显式explicit可以避免隐式转换。除非有很好的理由,那么一般将构造函数声明为显式explicit。
如下是隐式转换。

class A {
public:
	A(int v) {
		cout << "隐式" << endl;
	}
};
int main() {
	A a = 3; // 隐式地将3转化为A类对象
	return 0;
}

以下是显式的。

class A {
public:
	explicit A(int v) {
		cout << "隐式" << endl;
	}
};
int main() {
	A a(3); // 显式
	return 0;
}

十五. 浅拷贝与深拷贝

浅拷贝和深拷贝是针对指针而言的。
浅拷贝是指新的指针指向旧指针指向的内存,如果那块内存通过旧指针释放掉了,新指针也没用了;
深拷贝是指新的指针在内存中新开辟一块地方,把旧指针指向的内容复制到新的内存中,那么就算原阿里的内存被释放掉,新的指针还是可以在新开辟的内存中找到对应的对象。
深拷贝需要实现拷贝构造函数。系统默认的是浅拷贝构造函数。

十六. inline函数

inline函数是拿空间换时间的一种函数。
inline函数在编译时,直接嵌入到代码中,这样的好处是:避免了函数调用,时间上更快。这样的坏处是:代码更长,在内存中占用的空间更大。
所以只有在以下条件下不适合应用inline函数:
1.代码太长,内存消耗大;
2.函数中有循环,函数执行开销大。
在类中获取private或protected数据成员的函数可以写成inline函数。
另外,在类中声明的函数默认为内联函数。

十七. NULL和nullptr

在C++种,NULL存在二义性,既可以表示空指针,又可以表示为0。

int* p1 = NULL; // 表示空指针
int v1 = NULL; // 表示0

为了避免使用出错,用nullptr表示空指针。

十八. 操作符重载

有如下两种操作符重载的方式:
1.作为类成员

class A{
public:
	int a;
	A(int v): a(v) {};
	bool operator == (A& a) {\
		if (this->a == a.a) return true;
		return false;
	}
};

2.重载操作符函数

bool operator == (A& a, A& b) {
	if (a.a == b.a) return true;
	return false;
}

十九. 右值引用

C++11新增了移动,即把资源的所有权交给新对象,右值引用是为了将将亡值移动给新对象,从而减少构造函数和析构函数的调用次数,提高程序的性能。
以下代码中,getclassA()函数即形成了一个将亡值,通过&&创建一个右值引用:

A getclassA() {
	return A();
}
int main() {
	A && a = getclassA();
}

右值引用的常见应用场景是移动构造函数

class A{
public:
  A() = default;
	A(int size) : sz(size){
	}
    A(A & a) : sz(a.sz) {
        cout << "copy" << endl;
	}
	A(A && a) : sz(a.sz) {
        cout << "move" << endl;
	}

	int sz;
};
int main() {
	A a(3);
	A b = a; // 调用拷贝构造函数
	A c = move(a); // move将a变成右值,并调用移动构造函数
	A d(move(a)); // move将a变成右值,并调用移动构造函数
	return 0;
}

输出:
copy
move
move

二十. 零拷贝

零拷贝是一种避免CPU将数据从一块存储拷贝到另一块存储的技术,可以提高性能。
应用:vector的emplace_back()函数

class A
{
public:
	int v;
	A(int value) :v(value) { cout << "construction" << endl; }
	A(const A &a) :v(a.v) { cout << "copy construction" << endl; }
	A( A &&a) :v(a.v) { cout << "move construction" << endl; }
};

int main()
{
	cout << "constuction + copy-----" << endl;
	A a(1);
	vector<A> v1;
	v1.push_back(a);

	cout << "constuction + move-----" << endl;
	vector<A> v2;
	v2.push_back(A(2));

	cout << "only constuction-------" << endl;
	vector<A> v3;
	v3.emplace_back(11);

	return 0;
}

输出:
constuction + copy-----
construction
copy construction
constuction + move-----
construction
move construction
only constuction-------
construction

由此可见,采用emplace_back()只调用了一次构造函数,而其他方法会多调用一次拷贝构造函数或移动构造函数。

二十一. 多重继承与虚继承

多重继承是指一个派生类从多个基类继承而来。如以下所示:

class D: public B, public C {
...
};

优点:可以调用多个基类的接口。
缺点:
1.存在二义性。比如基类A和基类B都有同样的函数,那么C就不知道要调用哪一个。
解决方法:加上全局符。

cout << C::memberdata << endl;

2.如果类B和类C都是继承与类A,那么实例化D之后会有两份A的实例。
解决办法:虚拟继承。这样无论基类被继承多少次,只有一份实例。

class B: public virtual A {...};
class C: public virtual A {...};

二十二. 友元

友元分成两类,一类是友元函数,一类是友元类。
友元函数能够访问类的私有成员。

class A
{
public:
    A(double xx)
    {
        x = xx;
    }
    friend double show(A &a);
private:
    double x;
};

double show(A &a)
{
    cout << a.x << endl;
    return 1.0;
}

int main()
{
    A a(3.0);
    show(a);     //友元函数的调用方法,同普通函数的调用一样,不要像成员函数那样调用
    return 0;
}

友元类的所有函数默认都是友元函数。

你可能感兴趣的:(c++,开发语言)