[TOC]
关于C++头文件
导入符号使用细节
"xxx.h" 和<xxx.h> 都用于包含头文件(在编译器包含正确路径的前提下),"xxx.h" 优先从代码include文件(文件夹)中查找;<xxx.h> 优先从计算机已安装库里查找(如:Linux系统中
/include; /local/include)
此外常用的"xxx.h" 可用作相对位置查找(比较实用)
1 2 #include "../include/Log/Log.h"
避免重复导入
常用形式:
1 2 3 4 5 6 7 #ifndef _LOG_H #define _LOG_H #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
类视图效果(继承关系、函数重写、成员变量、成员函数)
类视图效果
调用堆栈
设置断点查看调用堆栈,看函数调用的由来
调用堆栈
关于循环
for 和while 没有特定的区分,看习惯使用。但通常需要经常改变某些变量,如遍历等操作用for ;需要一直循环,只在某些不常变条件 改变才终止循环的情况用while 。
控制流语句
循环经常搭配控制流语句 使用:continue 、break 、return ,前两个只对循环起作用,后一个直接结束函数。
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) { } 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 : int x, y; int speed; }; 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.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 : const int Red = 0 ; const int Green = 1 ; const int Blue = 2 ; private : int m_ColorType = 0 ; 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) { m_ColorType = colortype; std::cout<<"ColorType: " <<m_ColorType<<std::endl; } }; int main () { Player player (5 , 3 , 2 ) ; player.setColorType (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; 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 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 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; } } 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; std::cout<< offset << std::endl; }
避免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 }); vertices.push_back (vertex (4 , 5 , 6 )); vertices.push_back (vertex (7 , 8 , 9 )); return 0 ; }
预分配内存+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 ); vertices.emplace_back (1 , 2 , 3 ); vertices.emplace_back (4 , 5 , 6 ); vertices.emplace_back (vertex (7 , 8 , 9 )); return 0 ; }
关于处理多返回值
可以使用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 ; }
关于模板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; }
该代码相当于:
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; }
但需要注意,如果类内部用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]; 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 #if PR_DEBUG == 1 #define LOG(x) std::cout<< x << std::endl; #else #define LOG(x) #endif int main () { LOG ("Hello" ); }
关于将函数作为参数
原始函数指针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; 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) { 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;}; 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 #pragma once namespace chen{ extern int a; const int b = 2 ; void func () ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 #include "hello.h" #include <iostream> namespace chen{ int a = 5 ; static char c = 'c' ; 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 ; }