[TOC]

关于C++头文件

导入符号使用细节

"xxx.h"<xxx.h>都用于包含头文件(在编译器包含正确路径的前提下),"xxx.h"优先从代码include文件(文件夹)中查找;<xxx.h>优先从计算机已安装库里查找(如:Linux系统中 /include; /local/include)

此外常用的"xxx.h"可用作相对位置查找(比较实用)

1
2
#include "../include/Log/Log.h"
//表示从当前文件的上一级目录下的include/Log/中查找Log.h

避免重复导入

常用形式:

1
2
3
4
5
6
7
#ifndef _LOG_H
#define _LOG_H

//XXX
//XXX

#endif

可用简洁形式 #pragma once 代替(推荐)

检查重复导入作用,例如:在头文件中构造一个结构体,若不做重复导入避免处理,会报重定义错误。

关于软件调试

代码调试前提是需要生成DEBUG模式可执行文件。调试核心是断点读内存

断点

break point设置断点后,运行代码,程序会在第一个断点处暂停。注意此时断点行程序未被执行,是将要被执行。步进执行代码的黄色箭头也是如此,箭头所在行是将要被执行。

以Visual Studio为例

设置Debug模式并运行调试程序

设置Debug模式并运行

Continue继续执行到下一个断点或结尾;Step into进入函数内部;Step over跳到下一行;Step out跳出当前函数

Continue、Step into、Step over、Step out

调试运行后可以查看所有变量的值以及内存地址、内存中存储的值(以16进制显示,两位为一字节),鼠标停留可显示值。注意:若断点行是初始化赋值操作,代码运行到断点处该变量为未初始化状态(还未执行该行代码!)。在Visual Studio中DEBUG模式会为所有未初始化变量赋值0xCCCCCCCC(便于直观监控内存)(也就是十进制-858993460)

未初始化内存

autos、locals、watch窗口,展示变量、局部变量的值,监视变量的值(watch自己添加需要监视的变量)。字符串一般会同时显示地址和值。

watch窗口

菜单Debug->windows->memory->memory1查看内存存储情况。(&a表示取变量a地址,回车查看内存)

查看内存

Debug时跳出循环

注意跳出循环不能用Step out!这会直接跳出当前函数。可以在循环结束的下一句设置一个有效断点,然后运行Continue(可以在Debug运行中进行)。

关于源码阅读

先运行代码查看代码具体功能,然后看每个文件夹大致的作用(通过浏览文件夹名称以及所包含的文件名),最后查看类视图以及DEBUG设置合适断点查看栈帧。

以Visual Studio为例

查看类视图

类视图查看步骤1
类视图查看步骤2

类视图效果(继承关系、函数重写、成员变量、成员函数)

类视图效果

调用堆栈

设置断点查看调用堆栈,看函数调用的由来

调用堆栈

关于循环

forwhile没有特定的区分,看习惯使用。但通常需要经常改变某些变量,如遍历等操作用for;需要一直循环,只在某些不常变条件改变才终止循环的情况用while

控制流语句

循环经常搭配控制流语句使用:continuebreakreturn,前两个只对循环起作用,后一个直接结束函数。

continue:只能对循环使用。如果还有下一次迭代的话,表示进入下一次迭代,如果没有,循环就结束。

break:主要用于循环,也出现在switch语句中。表示跳出(终止)循环。

return:可以在任何地方,直接跳出当前函数。视当前函数返回值而定(返回void可以只写一个return)。

关于面向对象

比如一个游戏程序:我们要定义玩家1、玩家2、······,当不使用面向对象编程时会十分麻烦,要定义很多变量;对其操作也需要定义很多形参。就像下面的代码一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
void doSomething(int x, int y, int s)
{
//xxxxxxxxxx;
}

int main()
{
int PlayerX0, PlayerY;
int PlayerSpeed = 2;

int PlayerX1, PlayerY;
int PlayerSpeed = 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
class Player
{
public: //注意类的可见性,不设置则只能由public类成员函数访问类成员变量!
int x, y;
int speed;
}; //注意有分号,和结构体struct一样

void Move(Player& player, int xa, int ya)
//引用传值和指针一样,可以改变实参的内存值。(比指针简单明了,推荐)
{
player.x += xa * player.speed;
player.y += ya * player.speed;
}

int main()
{
Player player;
player.x = 5;
player.y = 3;
player.speed = 2;
Move(player, 2, 5);

Player playerX0;
Player playerX1;
std::cin.get(); //暂停控制台
}

方法也可写入对象

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
class Player
{
public:
int x, y;
int speed;
void Move(int xa, int ya)
//在函数内部就不需要传递对象了,直接使用对象的成员变量
{
x += xa * speed;
y += ya * speed;
}
};


int main()
{
Player player;
player.x = 5;
player.y = 3;
player.speed = 2;
player.Move(2, 5);

Player playerX0;
Player playerX1;
std::cin.get();
}

构造函数进一步简化对象初始化过程:

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
class Player
{
public:
//注意构造函数的可见性!
Player(int x_, int y_, int speed_):x(x_), y(y_), speed(speed_){}
//注意一旦写了自定义构造函数,默认构造函数就失效了,需要用的化需要再写一个!
Player(){}

int x, y;
int speed;
void Move(int xa, int ya)
//在函数内部就不需要传递对象了,直接使用对象的成员变量
{
x += xa * speed;
y += ya * speed;
}
};


int main()
{
Player player(5, 3, 2); //初始化一行搞定,一切为了简洁
//player.x = 5;
//player.y = 3;
//player.speed = 2;
player.Move(2, 5);

Player playerX0; //不自己再添加默认无参构造的话会报错的
Player playerX1;
std::cin.get();
}

本质上类是把一堆东西打包在一起了,并设置读写权限,所以有时我们可以灵活运用对象成员变量。比如:

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
class Player
{
public:
//将类型赋予名字,提高可读性
//最好用枚举enum Color { Red, Green, Blue };
const int Red = 0;
const int Green = 1;
const int Blue = 2;
private:
int m_ColorType = 0; //默认为红色;一般用m_表示私有
//可用枚举Color m_ColorType = Red;
public:
Player(int x_, int y_, int speed_):x(x_), y(y_), speed(speed_){}
Player(){}

int x, y;
int speed;
void Move(int xa, int ya)
{
x += xa * speed;
y += ya * speed;
}
void setColorType(int colortype) //形参可用枚举类型Color
{
m_ColorType = colortype;
std::cout<<"ColorType: "<<m_ColorType<<std::endl;
}
};


int main()
{
Player player(5, 3, 2);
//player.x = 5;
//player.y = 3;
//player.speed = 2;
player.setColorType(player.Green); //用自己的成员变量来设置
//枚举依然可以使用player.Green;最好用Player::Green
player.Move(2, 5);

Player playerX0;
Player playerX1;
std::cin.get(); //暂停控制台
}

关于静态变量static

分为两类:类外static和类内static

类外static

类外static意味着此变量只对其所在文件可见,重点:谨慎使用全局变量,尽量让函数和变量标记为静态的,除非真的需要它们跨文件(跨翻译单元)链接。或者使用命名空间

类内static

类内static意味着该变量将与类的所有实例共享内存,众多实例对象中该静态变量只有一个实例

包括静态方法也是如此,但类内静态方法实例化前就可以使用,且只能访问类中的静态成员(方法或属性)(因为静态方法没有类实例)。

类内static静态成员差不多不属于类了,需要在类外定义,不然会报错(方法其实不必须,因为不涉及内存。但依然可以作为习惯)

可以将类内static静态成员理解为是在一个命名空间(类名)中声明了一个变量或方法,区别是但它们依然可以设置public或private可见性(如静态成员变量私有,仅通过共有静态成员函数访问)。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Staticvar
{
public:
static int x, y;

static void hello()
{
std::cout<< x << y <<std::endl;
}
};

int Staticvar::x = 10; //全局区分配内存
int Staticvar::y; //注意此时不用static关键字

int main()
{
Staticvar::hello(); //静态成员函数当命名空间下函数使用
std::cout<<Staticvar::x<<Staticvar::y<<std::endl;
}

局部作用域内的static

{}作用域内(比如函数内)的static静态变量,意味着该变量只局部可见,但生命周期为整个程序运行过程。(类似于一个全局变量,但仅所处局部作用域内可见)(只初始化一次)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void function()
{
static int i = 0; //只执行一次初始化
i++;
std::cout<< i <<std::endl;
}

int main()
{
function();
function();
function();
function();
//结果输出1 2 3 4,而不是1 1 1 1
}

单例类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class singleton
{
public:
static singleton& Get()
//使用复制的话可以不用static,但这样占内存
//故返回值使用静态变量,接收用 &
//函数用static表明函数可用类直接调用,该函数不属于任何类实例
{
//通过static延长单例的生命周期
//因为不用static单例内存在栈上,返回栈上变量大有问题!
static singleton instance;
return instance;
}

void hello(){std::cout<<"success!"<<std::endl;}
};

int main()
{
singleton::Get().hello();
std::cin.get();
}

局部类

注意定义在函数内或者{}作用域内的叫局部类,局部类可以有静态成员函数,但不能有静态成员变量(函数内的内存在堆栈上(局部静态变量除外),而静态成员变量在编译时就要分配空间,在全局数据区。编译时不知道有该局部类,所以无法分配局部类下的静态内存)

关于构造函数

如果不希望创建的类实例化对象,则可以把构造函数设置为private私有函数(此时创建对象会报错)。或者将函数=delete;如对于Log类,公有属性中加 Log() = delete; 即可。

关于继承

注意:private私有成员变量或成员函数,即使子类用public公有继承也无法访问。(因为private只有自己或友元可以访问)。所以就有了protect,可以自己、友元或者子类中访问,但类外不可访问。

虚函数

使用虚函数时,父类在函数前virtual关键字,子类重写在函数后使用override关键字(可以不用,但用会更清楚)

接口(纯虚函数)

(虚函数 = 0;)纯虚函数也称为接口,必须在子类中进行重写才能实例化对象。

关于智能指针

都建议使用std::unique_ptr< > xx = std::make_unique< >() / std::shared_ptr< > xx = std::make_shared< >() / weak_ptr用shared_ptr赋值。注意包含头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <memory>

int main()
{
{
std::shared_ptr<int> sherad_e0;
{
std::unique_ptr<int> unique_e1 = std::make_unique<int>();
std::shared_ptr<int> shared_e2 = std::make_shared<int>();
sherad_e0 = shared_e2;
std::weak_ptr<int> weak_e3 = sherad_e0;
} //unique_ptr死
} //shared_ptr死,weak_ptr也死
return 0;
}

unique_ptr

首选unique_ptr(作用域指针,出作用域即死,释放内存)(所以不能进行指针复制,不能两个unique_ptr指向一个内存,不然有重复释放风险(已删除拷贝构造和=复制,使用即报错))

shared_ptr

需要拷贝指针时(如函数传参等)则使用shared_ptr(可以多个shared_ptr指向同一块内存,使用引用计数,最后一个指针出作用域才死)

weak_ptr

weak_ptr可以复制shared_ptr,但不影响引用计数,所以不影响内存生死

关于函数传值

请总是用const XXX & 常引用传值,这会优化程序性能(并且可以接收临时右值),可以在函数内部决定要不要copy,让copy发生在函数内部而不是传值的时候!

获取类成员变量的内存偏移量

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
struct vector3
{
float x, z, y;
};
int main()
{
long offset = (long) &((vector3*)0)->z;
//32位系统可以把指针转为int
//即int offset = (int) &((vector3*)nullptr)->z;
std::cout<< offset << std::endl;
}
//输出4

避免std::vector复制(使用优化)

没有使用预分配内存 .reserve(n)emplace_back 时:

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
#include <iostream>
#include <vector>

class vertex
{
public:
float x, y, z;
vertex(float x, float y, float z)
:x(x), y(y)
{
this->z = z;
}

vertex(const vertex& v) //拷贝构造
:x(v.x), y(v.y), z(v.z)
{
std::cout<<"copied!"<<std::endl;
}
};

int main()
{
std::vector<vertex> vertices;
vertices.push_back({1, 2, 3});
//隐式转换再复制
//容积为1,复制一次

vertices.push_back(vertex(4, 5, 6));
//构造(也有int->float的隐式转换),然后复制
//新分配容积2,旧内存复制到新内存 + 新插入,复制两次

vertices.push_back(vertex(7, 8, 9));
//新分配容积3,旧内存复制到新内存 + 新插入,复制三次

return 0;

//总共复制6次,输出:
/*
copied!
copied!
copied!
copied!
copied!
copied!
*/
}

预分配内存+emplace_back

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
#include <iostream>
#include <vector>

class vertex
{
public:
float x, y, z;
vertex(float x, float y, float z)
:x(x), y(y)
{
this->z = z;
}

vertex(const vertex& v) //拷贝构造
:x(v.x), y(v.y), z(v.z)
{
std::cout<<"copied!"<<std::endl;
}
};

int main()
{
std::vector<vertex> vertices;
vertices.reserve(3); //不需反复扩容,本例少3次copy

vertices.emplace_back(1, 2, 3);
//直接在内存里构造,没有复制
vertices.emplace_back(4, 5, 6);

vertices.emplace_back(vertex(7, 8, 9));
//这种构造与 push_back 一样,复制一次

return 0;
//总共复制1次,输出:
/*
copied!
*/
}

关于处理多返回值

可以使用c++的特定类型tuple、pair等进行返回,但其取值不直观(.first .second不能直观表达变量含义),或者可以使用引用传参、指针传参,但其传参参数太多(不简洁),同类型可以返回数组或vector(但仅限于同类型)。

struct返回

故推荐使用struct(或聚合类)进行返回,类中成员变量可按需命名,并随意添加,最后返回可用大括号{},因为聚合类可用大括号赋值,十分方便明了。例:

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>
#include <string>

struct Retpara
{
float speed;
float posititon;
std::string carName;
};

Retpara func()
{
float fspeed = 1.f;
float fposititon = 2.f;
std::string fcarName = "mycar";

return {fspeed, fposititon, fcarName};
//返回方便直观
}

void print(float pspeed, float pposition, std::string pcarName)
{
std::cout<<pspeed<<std::endl;
std::cout<<pposition<<std::endl;
std::cout<<pcarName<<std::endl;
}

int main()
{
struct Retpara retpara = func();
print(retpara.speed, retpara.posititon, retpara.carName);
//传参方便直观
return 0;
/*输出:
1
2
mycar
*/
}

关于模板template

模板不仅可以在编译时根据调用代码实例化函数,类等,还可以在编译时根据调用确认参数值,如:

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

template<typename T, int N>
class Array
{
private:
T m_array[N];
public:
int getSize()const
{
return N;
}
};
int main()
{
Array<int, 5> array;
//调用编译时生成具体类代码,无调用不生成类
//调用编译时确认参数值
std::cout << array.getSize() << std::endl;
//输出:5
}

该代码相当于:

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

template<typename T>
class Array
{
private:
T m_array[5];
public:
int getSize()const
{
return sizeof(m_array)/sizeof(int);
}
};
int main()
{
Array<int> array;
std::cout << array.getSize() << std::endl;
//输出:5
}

但需要注意,如果类内部用int size = 5;等语句时,这时候不是初始化,是设置构造对象时的默认值。这时候size并没有分配内存,并没有值(除静态成员变量外,类不能分配内存),所以不能用size去给内部别的变量赋值,如下代码就是错的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//!!!错的用法!!!
template<typename T>
class Array
{
private:
int size = 5;
T m_array[size];
//此时size并没有值,所以该用法是错的
// error: invalid use of non-static data member ‘Array::size’
// | T m_array[size];
public:
int getSize()const
{
return sizeof(m_array)/sizeof(int);
}
};

关于宏

常用于调试,调试模式下可以向控制台输出一些信息,发布模式则可自动删除打印信息等调试代码(最好define时给一个值):

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

#define PR_DEBUG 1
//可以在编译时define
//实现一个值自动改变代码构成

#if PR_DEBUG == 1
//预处理主要针对#内容
#define LOG(x) std::cout<< x << std::endl;
//#elif defined(PR_RELEASE) //推荐
#else
#define LOG(x) //定义为空
#endif

int main()
{
LOG("Hello");
//#define PR_DEBUG 1 输出:Hello
//#define PR_DEBUG 0 输出:
}

关于将函数作为参数

原始函数指针C

构造函数指针类型时只需将函数名替换为*变量,如:

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

void Helloworld(int a)
{
std::cout<< a << std::endl;
}
int main()
{
void(*func)(int a) = Helloworld;
//构造函数指针类型时形参名其实可以不写,保留形参类型就行

//或者用typedef
//typedef void(*Helloworldfunc)(int);
//Helloworldfunc func = Helloworld;
func(5);

std::cin.get();
return 0;
}

C++中的std::function

用lambda表达式时,如果使用捕获功能则不能用原始C的函数指针,原始函数无捕获能力。此时需要使用更加方便的std::function,注意包含头文件functional,例:

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>
#include <vector>
#include <functional>

void forEach(std::vector<int> values,const std::function<void(int)>& func)
//注意只有常量引用才能指向临时变量,lambda表达式是匿名函数,应该属于临时变量
{
for(int value : values)
func(value);
}
int main()
{
std::vector<int> values = {1, 4, 2, 5, 3};
int a = 4;
auto lambda = [=](int value)mutable {a = 5; std::cout<< value << a << std::endl;};
//注意此处=为复制捕获作用域内所有变量,但不加mutable不能修改捕获的变量
//加了mutable可以修改捕获的变量,(虽同名)但修改的是复制品,不影响原变量的值
//使用 &a 捕获a可以修改a的值,因为是引用捕获
forEach(values, lambda);

std::cin.get();
return 0;
}

关于lambda表达式

也称匿名函数,形如 [ ] ( ) { } [ ] ( ) -> { }

[ ] 中括号内是捕获所在作用域的变量(注意全局变量不需要捕获

  • = 为值传递捕获所有变量(注意用mutable才能修改捕获的复制品值,如上)
  • & 为引用传递捕获所有变量,引用不复制,且可以影响原变量
  • a, &b 为值传递捕获a,引用传递捕获b。(注意值传递捕获需要mutable才能修改复制值)
  • 特别的,[this]表示捕获当前的this指针,常用于class内部的lambda

( ) 小括号中是函数形参

{ } 花括号中是函数体

-> 后是函数返回值类型,当返回值是void或者函数体内只有一处return时(返回值明确),可以省略->, 基本都可以省略

关于namespace

首先,尽量不要使用using namespace std等(可以在很局部的作用域使用),防止同名函数调用冲突,用了相当于抹杀namespace作用。

namespace:命名空间、名称空间。一般在头文件和相应的cpp文件使用,相当于对变量、函数名称套一个“姓”

使用命名空间中的变量、函数时,需要用全名,(姓::名),其中::为域解析符,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//hello.h
//注意尽量少用全局变量
#pragma once
namespace chen{
extern int a;
const int b = 2;
//全局变量,所有文件有效,想要在别的文件使用一定要用exterm
//其他文件引此头文件声明多次,但只在对应的cpp文件定义一次!
//const全局常量可以直接在头文件直接定义,因为不会改变了!
//建议不加extern,所有文件各一份,因为在头文件定义了,多次引用该头文件会报重定义!
//也可和静态全局变量一样在cpp文件内使用,推荐!

//不要在头文件定义静态全局变量static char c; !
//static静态变量声明与定义同时进行,所以不能与exterm同时出现!
//静态全局变量只在当前文件有效,所以一般直接放到对应的cpp文件!
void func();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
//hello.cpp
#include "hello.h"
#include <iostream>
namespace chen{
int a = 5;
//全局变量头文件声明,cpp文件定义,注意定义时只少一个extern
static char c = 'c';
//静态变量cpp文件自己定义自己使用!不关头文件的事!
void func()
{
std::cout<< a <<" "<< b <<" "<< c <<std::endl;
}
}

main使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "hello.h"
#include <iostream>
int main()
{
chen::func();
chen::a = 22;
std::cout<<"a: "<<chen::a<<std::endl;
std::cin.get();
return 0;
/*输出:
5 2 c
a: 22
*/
}