C++概念

C与C++的区别

  1. C++是对C语言的扩充和延伸,并且对C语言提供后向兼容的能力。
  2. C++由面向过程编程、面向对象编程、泛型编程、元编程、函数式编程、STL标准库组成。
  3. C++的出现是为了更方便地开发大型应用程序,面向对象编程里的很多重要思想和机制都对大型项目和复杂系统所要求的项目工程化、代码复用性/扩展性/可维护性等提供了强大的支撑。

C++与Java的区别

  1. Java的主要应用在应用层,C++主要在中间件和底层。
  2. Java取消了指针带来更高的代码质量;完全面向对象,独特的运行机制是其具有天然的可移植性。
  3. Java是运行在JVM上的。因为JVM是可以跨平台安装运行的,所以Java的可移植性强。
  4. C++在不同的系统上运行,需要不同的编码。
  5. 垃圾回收机制的区别。C++用析构函数回收对象垃圾,java使用GC算法去回收对象垃圾。

C++与Python的区别

  1. C++是高级语言/编译型语言,Python是脚本语言/解释型语言。

    高级语言:可以使用解释、编译两种方式执行。

    脚本语言: 缩短了编译-链接-运行过程。在运行时解释执行。

    编译型语言:先将源代码编译成目标语言之后通过连接程序连接到生成的目标程序进行执行。

    解释型语言:根据输入数据当场执行,不生成目标程序。在终端打一条命令和语句,解释程序就立即将该语句解释成一条或多条几条指令提交给硬件立即执行。

  2. C++和Python都是强类型语言。

    弱类型:能够直接进行隐式转换。

C++的三大特性

继承

被继承的是父类(基类),继承出来的类是子类(派生类),子类拥有父类的所有的特性。
继承方式有公有继承、私有继承,保护继承。默认是私有继承。
C++语言允许单继承和多继承。
优点:继承减少了重复的代码、继承是多态的前提、继承增加了类的耦合性。
缺点:继承在编译时刻就定义了,无法在运行时刻改变父类继承的实现;父类通常至少定义了子类的部分行为,父类的改变都可能影响子类的行为;如果继承下来的子类不适合解决新问题,父类必须重写或替换,那么这种依赖关系就限制了灵活性,最终限制了复用性。
虚继承:为了解决多重继承中的二义性问题,它维护了一张虚基类表。

多态

  1. 多态是指同一操作作用于不同的对象,可以产生不同的解释和不同的执行结果。

  2. 多态分为编译时多态和运行时多态。

    编译时多态:函数重载和运算符重载。即在同一个作用域中,函数名相同但参数不同的函数称为重载函数。运算符重载是指可以重新定义运算符的操作数类型和操作数个数。编译时多态的函数调用机制是编译器在编译时就确定了调用哪个函数,所以他是静态的。

    运行时多态:虚函数和纯虚函数。即在基类中存在虚函数(一般为纯虚函数)子类通过重载这些接口,使用基类的指针或者引用指向子类的对象,就可以调用子类对应的函数,动多态的函数调用机制是执行期才能确定的,所以他是动态的。

优点:大大提高了代码的可复用性;提高了了代码的可维护性,可扩充性。
缺点:易读性比较不好,调试比较困难。模板只能定义在头文件中,当工程大了之后,编译时间十分的变态。

封装

  1. 隐藏类的属性和实现细节,仅对外提供接口。

  2. 封装性实际上是由编译器去识别关键字public、private和protected来实现的,体现在类的成员可以有公有成员(public),私有成员(private),保护成员(protected)。

    私有成员是在封装体内被隐藏的部分,只有类体内说明的函数(类的成员函数)才可以访问私有成员,而在类体外的函数时不能访问的。

    公有成员是封装体与外界的一个接口,类体外的函数可以访问公有成员。

    保护成员是只有该类的成员函数和该类的派生类才可以访问的。

优点:隔离变化;便于使用;提高重用性;提高安全性。

缺点:如果封装太多,影响效率;使用者不能知道代码具体实现。

C++的垃圾回收机制

C语言本身没有提供GC机制,而C++ 0x则提供了基于引用计数算法的智能指针进行内存管理。

引用计数

基本思路是为每个对象加一个计数器,计数器记录的是所有指向该对象的引用数量。每次有一个新的引用指向这个对象时,计数器加一;反之,如果指向该对象的引用被置空或指向其它对象,则计数器减一。当计数器的值为0时,则自动删除这个对象。

缺点:有循环引用问题;多个线程同时对引用计数进行增减时,引用计数的值可能会产生不一致的问题。

标记清除

Mark&Sweep垃圾收集器由标记阶段和回收阶段组成,标记阶段标记出根节点所有可达的对节点,清除阶段释放每个未被标记的已分配块。典型地,块头部中空闲的低位中的一位用来表示这个块是否已经被标记了。通过Mark&Sweep算法动态申请内存时,先按需分配内存,当内存不足以分配时,从寄存器或者程序栈上的引用出发,遍历上述的有向可达图并作标记(标记阶段),然后再遍历一次内存空间,把所有没有标记的对象释放(清除阶段)。因此在收集垃圾时需要中断正常程序,在程序涉及内存大、对象多的时候中断过程可能有点长。当然,收集器也可以作为一个独立线程不断地定时更新可达图和回收垃圾。

缺点:该算法不像引用计数可对内存进行即时回收;在分配大量对象时,且对象大都需要回收时,回收中断过程可能消耗很大。

优点:解决了引用计数的循环引用问题。

节点复制

从根节点开始,被引用的对象都会被复制到一个新的存储区域中,而剩下的对象则是不再被引用的,即为垃圾,留在原来的存储区域。释放内存时,直接把原来的存储区域释放掉,继续维护新的存储区域即可。

缺点:需要两倍的内存空间;复制的过程会消耗大量的时间。

优点:当需要回收的对象越多时,它的开销很小。

分代收集

程序中存在大量的这样的对象,它们被分配出来之后很快就会被释放,但如果一个对象分配后相当长的一段时间内都没有被回收,那么极有可能它的生命周期很长,尝试收集它是无用功。为了让GC变得更高效,我们应该对刚诞生不久的对象进行重点扫描,这样就可以回收大部分的垃圾。

为了达到这个目的,我们需要依据对象的”年龄“进行分代,刚刚生成不久的对象划分为新生代,而存在时间长的对象划分为老生代,根据实现方式的不同,可以划分为多个代。
首先从根开始进行一次常规扫描,扫描过程中如果遇到老生代对象则不进行递归扫描,这样可大大减少扫描次数。这个过程可使用标记清除算法或者复制收集算法。然后,把扫描后残留下来的对象划分到老生代,若是采用标记清除算法,则应该在对象上设置某个标志位标志其年龄;若是采用复制收集,则只需要把新的存储区域内对象设置为老生代就可以了。

malloc使用的垃圾回收机制

当应用程序使用malloc试图从堆上获得内存块时,通常都是以常规方式来调用malloc,而当malloc找不到合适空闲块的时候,它就会去调用垃圾收集器,以回收垃圾到空闲链表。此时,垃圾收集器将识别出垃圾块,并通过free函数将它们返回给堆。这样看来,垃圾收集器代替我们调用了free函数,从而让我们显式分配,而无须显式释放。

垃圾收集器为一个保守的垃圾收集器。保守的定义是:每个可达的块都能够正确地被标记为可达,而一些不可达块却可能被错误地标记为可达。其根本原因在于C/C++语言不会用任何类型信息来标记存储器的位置,即对于一个整数类型来说,语言本身没有一种显式的方法来判断它是一个整数还是一个指针。因此,如果某个整数值所代表的地址恰好的某个不可达块中某个字的地址,那么这个不可达块就会被标记为可达。所以,C/C++所实现的垃圾收集器都不是精确的,存在着回收不干净的现象。而像Java的垃圾收集器则是精确回收。

C++中的指针和引用

C++中的对象:对象是指一块能存储数据并具有某种类型的内存空间。一个对象a,它有值和地址&a,运行程序时,计算机会为该对象分配存储空间,来存储该对象的值,我们通过该对象的地址,来访问存储空间中的值。
指针:指针p也是对象,它同样有地址&p和存储的值p,只不过,p存储的数据类型是数据的地址。如果我们要以p中存储的数据为地址,来访问对象的值,则要在p前加解引用操作符”“,即p。
引用:引用理解成变量的别名。定义一个引用的时候,程序把该引用和它的初始值绑定在一起,而不是拷贝它。计算机必须在声明r的同时就要对它初始化,并且,r一经声明,就不可以再和其它对象绑定在一起了。

使用指针的情况:

  1. 如果你有一个变量,目的是指向另一个变量,但是也有可能它不指向任何对象的时候。
  2. 如果你有一个变量,目的是在不同的时间指向不同对象的时候。

使用引用的情况:

  1. 如果这个变量总是必须代表一个对象,该变量不能为null。
  2. 如果这个变量总是代表一个对象,并且一旦代表了这个对象就不会改变。

C++11 语言特性

1.1 nullptr和std::nullptr_t

C++11允许你使用nullptr取代0或者NULL,用来表示一个pointer(指针)指向no value。例如:

1
2
3
4
5
void f(int);
void f(void*);
f(0); // call f(int)
f(NULL); // call f(int) if NULL is 0 ambigous otherwise
f(nullptr); // call f(void*)

nullptr会被自动转换成各种pointer类型,但不会被转换成任何整数类型

1.2 以auto完成类型自动推导

auto类型推导

1
2
3
4
5
6
auto x = 5;                 // 正确,x是int类型
auto pi = new auto(1); // 正确,p是int*
const auto* v = &x, u = 6; // 正确,v是const int*类型,u是const int
static auto y = 0.0; // 正确,y是double类型
auto int r; // 错误,auto不在表示存储类型的指示符
auto s; // 错误,auto无法推导出s的类型(必须马上初始化)

auto并不能代表一个实际的类型声明(上面s编译错误),只是一个声明类型的“占位符”。使用auto声明的变量必须马上初始化,让编译器推断出它的类型,并且在编译时将auto占位符替换成真正的类型。

auto推导规则

1
2
3
4
5
6
7
8
9
int x = 0;
auto *a = &x; // a -> int*,a被推导为int*
auto b = &x; // b -> int,b被推导为int,忽略了引用
auto &c = x; // c -> int&,c被推导为int&
auto d = c; // d -> int,d被推导为int,忽略了引用
const auto e = x; // e -> const int
auto f = e; // f -> int
const auto& g = x; // g -> const int&
auto& h = g; // h -> const int&

在不声明为引用或者指针时,auto会忽略等号右边的引用类型和const限定
在声明为引用或者指针时,auto会保留等号右边的引用和const属性

auto的限制

  1. auto的使用必须马上初始化,否则无法推导出类型
  2. auto在一行定义多个变量时,各个变量不能产生二义性,否则编译失败
  3. auto不能用作函数的参数
  4. 在类中auto不能用作非静态成员变量
  5. auto不能定义数组,可以定义指针
  6. auto无法推断出模板参数

1.3 decltype用于推导表达式类型

decltype用于推导表达式类型,这里只用于编译器分析表达式的类型,表达式实际不会进行运算

1
2
3
4
5
int func() { return 0; }
decltype(func()) i; // i为int类型
int x = 0;
decltype(x) y; // y为int类型
decltype(x + y) z; // z为int类型

注意:decltype不会像auto一样忽略引用和const属性,decltype会保留表达式引用和cv属性

decltype推导规则

  1. exp是表达式,decltype(exp)和exp类型相同
  2. exp是函数调用,decltype(exp)和函数返回值类型相同
  3. 其他情况,若exp是左值,decltype(exp)是exp的左值引用

auto和decltype配合使用

1
2
3
4
5
template<typename T, typename U>
return_value add(T t, U u) {
// t和u类型不确定,无法推导出return_value类型
return t + u
}

改进为

1
2
3
4
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
return t + u
}

1.4 一致性初始化与初值列

一致性初始化

面对任何初始化动作,可以用大括号进行初始化

1
2
3
int values[] {1, 2 ,3};
std::vector<int> v {2, 3, 5, 7, 11, 13, 17};
std::vector<std::string> cities {"beijing", "location", "cario"};

初值列

即使某个local变量属于某种基础类型,也会被初始化为0或者nullptr(如果是个指针)

1
2
3
4
int i;      // i has undefined value
int j{}; // j is initalized by 0
int *p; // p has undefined value
int *q{}; // j is initalized by nullptr

注意:窄化–精度降低或造成数值变动,对大括号而言是不成立的

1
2
3
4
5
6
7
8
int x1(5.3);                            // ok, x1 -> 5
int x2 = 5.3; // ok, x2 -> 5
int x3{5.0}; // error
int x4 = {5.0}; // error
char c1{7}; // ok
char c2{99999}; // error
std::vector<int> {1, 2, 3, 4, 5}; // ok
std::vector<int> {1, 2, 3, 4, 5}; // error

std::initalizer_list<>

initalizer_list<>用来支持(a list of value)进行初始化 \

实例1:

1
2
3
4
5
6
void print(std::initalizer_list<int> vals) {
for (auto p = vals.begin(); p != vals.end(); ++p) {
std::cout << *p << ' ';
}
}
print({12, 3, 4, 5, 6, 7, 8});

实例2:

1
2
3
4
5
6
7
8
9
class P {
public:
P(int, int);
P(std::initalizer_list<int>);
};
P p{77, 5}; // Calls P::P(int, int)
P q{77, 5}; // Calls P::P(initalizer_list)
P r{77, 5, 42}; // Calls P::P(initalizer_list)
P s = {77, 5, 42}; // Calls P::P(initalizer_list)

实例3:
explicit构造函数如果接受的是一个初值列,会失去初值列带有0个,1个,多个初值的能力

1
2
3
4
5
6
7
8
9
10
class P {
public:
P(int, int);
explicit P(std::initalizer_list<int>);
};
P p{77, 5}; // ok
P q{77, 5}; // ok
P r{77, 5, 42}; // ok
P s = {77, 5, 42}; // error
P s = {77, 5} // ok

1.5 Range-Based for循环(foreach循环)

语法特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for (decl : coll) { // decl是给定值coll集合中每一个元素的声明
statement
}

for (int i : {2, 3, 5, 7, 9, 13, 17, 19}) {
std::cout << i << std::endl;
}

std::vector<double> vec;
for (auto& elem : vec) {
elem *= 3;
}

// 打印某集合内所有元素的泛型函数实例
template<typename T>
void printElements(const T& coll) {
for (const auto& elem : coll) {
std::cout << elem << std::endl;
}
}

可以针对初值列使用range-based for循环,因为class template std::initalizer_list<>提供了成员函数begin()和end()

1
2
3
4
5
6
7
8
int array[] = {1, 2, 3, 4, 5};
long sum = 0;
for (int x : array) {
sum += x;
}
for (auto elem : {sum, sum * 2, sum * 4} {
std::cout << elem << std::endl;
})

1.6 move语义和左值引用

###为何需要移动语义
假设有如下代码:

1
2
3
4
vector<string> vstr;
// build up a vector of 20,000 strings, each of 1000 characters
...
vector<string> vstr_copy1(vstr); // make vstr_copy1 a copy of vstr

vector和string类都使用了动态内存分配,因此它们必须定义某种new版本的复制构造函数,为初始化对象vstr_copy1,复制构造函数vector将使用new给20000个string对象分配内存,而每个string对象又将调用string的复制构造函数,该构造函数使用new为1000个字符分配内存。接下来全部20000*1000个字符都将从vstr控制的内存中复制到vstr_copy1控制的内存中

假设以下面方式使用它:

1
2
3
4
vector<string> vstrl
// build up a vector of 20,000 strings, each of 1000 characters
vector<string> vstr_copy1(vstr);
vector<string> vstr_copy2(allcaps(vstr));

从表面上看,两个复制一致,它们都使用了一个现有的对象初始化一个vector对象,如果深入探索这些代码,将发现allcaps()创建了对象temp,该对象管理着200001000个字符,vector和string的复制构造函数创建这200001000个字符的副本,然后程序删除allcaps()返回临时对象。(这里做了很多无用功)

使用移动语义

编译器对数据的所有权直接转让给vstr_copy2,不进行新的复制副本,在删除副本;而是将字符留在原来的地方,并将vstr_copy2与之相关联

如何实现移动语义

使用右值引用,让编译器知道什么时候需要复制,什么时候不需要

方法:使用移动构造函数,它使用右值引用作为参数,该引用关联到右值实参
注意1:复制构造函数可执行深复制,而移动构造函数只调整记录
注意2:在将所有权转移给新对象的过程中,移动构造函数可能修改其实参,这意味着右值引用参数不应该是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
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
66
67
68
69
70
71
72
73
74
75
#include <iostream>
using namespace std;

class Useless {
private:
int n;
char *pc;
static int ct;
void ShowObject() const;

public:
Useless();
explicit Useless(int k);
Useless(int k, char ch);
Useless(const Useless &f);
Useless(Useless &&f);
~Useless();
Useless operator+(const Useless &f) const;
void ShowData() const;
}

int Useless::ct = 0;

Useless::Useless {
++ct;
n = 0;
pc = nullptr;
ShowObject();
}

Useless::Useless(int k) : n(k) {
++ct;
pc = new char[n];
ShowObject();
}

Useless::Useless(const Useless& f) : n(f.n) {
++ct;
pc = new char[n];
for (int i = 0; i < n; i++) {
pc[i] = ch;
}
ShowObject();
}

Useless::Useless(Useless&& f) : n(f.n) {
++ct;
pc = f.pc;
f.pc = nullptr;
for (int i = 0; i < n; i++) {
pc[i] = ch;
}
ShowObject();
}

Useless::~Useless() {
cout << "destructor called; objects left: " << --ct << endl;
cout << "deleted object:\n";
ShowObject();
delete []pc;
}

Useless Useless::operator+(const Useless& f) const {
cout << "Entering operator+()\n";
Useless temp = Useless(n + f.n);
for (int i = 0; i < n; i++) {
pc[i] = ch;
}
for (int i = 0; i < temp; i++) {
temp.pc[i] = f.pc[i - n];
}
cout << "temp object:\n";
cout << "Leaving operator+()\n";
return temp;
}

两种构造函数比较

复制构造函数:

1
2
3
4
5
6
7
Useless::Useless(const Useless& f) : n(f.n) {
++ct;
pc = new char[n];
for (int i = 0; i < n; i++) {
pc[i] = ch;
}
}

移动构造函数:

1
2
3
4
5
6
Useless::Useless(const Useless&& f) : n(f.n) {
++ct;
pc = f.pc;
f.pc = nullptr;
f.n = 0;
}

它让pc指向现有的数据,以获取这些数据的所有权,此时pc和f.pc指向相同的数据,调用析构函数时将带来麻烦,因为程序不能对同一个地址调用delete两次,为避免这个问题该构造函数将原来的指针设置为空指针(对空指针执行delete[]没有问题)

由于修改了f对象,这要求不能在参数声明中使用const

Hexo入门以及如何部署到Github

这篇文章主要介绍了如何在本地快速搭建Hexo框架下的博客以及如何将自己的博客部署到Github上。

快速开始

安装Hexo

前提是本地已经安装了npm,然后最好在本地先新建一个文件夹,然后运行下面的命令安装Hexo

1
$ npm install hexo-cli -g

个人是在ubuntu系统下进行的操作,可能直接运行会安装失败,可以尝试在命令前添加sudo

安装你的博客

1
2
$ hexo init blog
$ cd blog

生成静态文件

1
$ hexo generate 或者 hexo g

启动服务

1
$ hexo server 或者 hexo s

在启动服务后你就可以在你的浏览器中输入https://hexo.io/docs/one-command-deployment.html 来预览你的博客了

清除文件

1
$ hexo clean

创建新的博文

1
$ hexo new "BlogName"

进行自定义操作

更换Hexo默认主题

下载主题

首先在Github上找到你所喜欢的主题的仓库,git clone主题仓库到你所创建博客的blog/themes文件夹,在这里我选用的是star数最高的next主题

1
$ git clone https://github.com/theme-next/hexo-theme-next themes/next

然后你可以进入到next文件夹下的_config.yml文件下修改参数以达到你想要的主题效果

更换主题

打开博客创建文件夹blog,找到_config.yml文件并打开,修改theme参数

1
theme: next

部署博客到Github

创建Github仓库

在这里就不展开叙述了,在自己的github账号下创建一个用户名.github.io仓库,网上有很多教程。

安装hexo-deployer-git

1
$ npm install --save hexo-deployer-git

修改配置文件

打开博客创建文件夹blog,找到_config.yml文件并打开,添加参数

1
2
3
4
deploy:
type: git
repository: https://github.com/用户名/用户名.github.io.git
branch: master

配置git用户名和邮箱

如果已经配置过,忽略这一步。打开git bash

1
2
$ git config --global user.name "用户名"
$ git config --global user.email "邮箱"

部署博客到github.io仓库

1
$ hexo d

期间会要求输入你的Github用户名和密码