I will share my cpp learning experience in this project.
And each branch of it will be a full test with the readme doc.
- 常量必须被初始化。
- 可以在运行时给常量赋初值。
- 如果想要使用全局的const:
- 不论声明还是定义都需要在 const 前加入
extern。 - 全部
cpp文件中只能有一处初始化。 - 其他文件需要声明后使用,一般把声明放在
.h文件中。
- 不论声明还是定义都需要在 const 前加入
- 通过下面语句可以创建一个只读的 const 变量。
const int var = somthing;- high-level const 不可以改变自身的指向
int *const var = &somthing;- low-level const 不可以改变指向的值
const int *var = &somthing;- 什么是常量表达式?
- 字面值
- 用常量表达式初始化的 const 对象
- 常量表达式函数
- 用
constexpr修饰 - 定义时约束
- 返回值必须是字面值
- 作为赋值对象付给某个常量构成新的常量表达式时约束:
- C++11
- 函数体必须只包含一个return语句
- 函数提可以包含其他的语句,但是这些语句不能在运行期起作用
- 函数可以不返回常量,但是在调用的时候实参必须传入常量表达式
- C++14
- 无 C++11 那些
- C++11
- 正常情况下使用无约束
- 用
// constexpr 的功能: 约束返回值必须为字面值;当这个函数作为另一个 constexpr 的赋值者时,约束参数必须为 constexpr
constexpr int something(int var){return var;}
int var = 1;
// 是合法的
something(var);
// 是非法的
constexpr int ret = something(var);
- 不带自动匹配
顶层const修饰 - 自动匹配
底层 const修饰 - 不自动匹配引用
- 当使用
&表示引用时,自动包含本体的 const 属性(如果有的话)
作用:通过某个表达式判断其类型,用于声明定义(不计算表达时的值)。
- decltype 自动匹配
引用、顶层 const - 自动匹配
底层 const修饰 - 不自动匹配引用
- 对指针解引的类型是该类型的引用
auto var = 1, &ref_var = var, *p = &var; //对指针解引的类型是引用 decltype(*p) test_p = *p; test_p = 10;
- 给变量加上括号就是引用类型
auto var = 1, &ref_var = var, *p = &var; decltype((var)) ref = *p;
- 赋值语句是引用类型
- 会默认初始化(各成员被初始化成0或者空串)
- 使用花括号初始化
- 花括号初始化内置类型时,如果有数据丢失风险会报错,结构体不会
- 也可以使用等号后在使用花括号,不过这个多了赋值过程
- 每个头文件都加上判断重定义宏
- 直接使用
std::string var_string = "something";- 单独声明使用一个命名空间变量
using std::string;- 声明使用完整的命名空间
using namespace::std;- 默认初始化
// 默认初始化
string default_init_var;- 拷贝初始化
// 拷贝初始化
string copy_init_var = "copy";
string copy_init_var1 = construct_init_var1;- 直接初始化
// 直接初始化
string construct_init_var("test");
string construct_init_var1(10,'c');- 重载
>>,<<:覆盖式赋值,输出
string test;
// 从 cin 中读一个字符串给test,遇到空白符为止,最后一个非空白符后面的空白符全部都会留在 cin 中
cin >> test;
cout << test << std::endl;getline(is,a):从 is 中读一行赋值给 a,实测不会截断前后方的空白符。s.empty(): 判断字符串是否是空串s.size(): 字符串有多少个字符+:连接- 使用两个字面值是无法使用加号连接的
=:覆盖==:如果字符完全一样就相等
for(declaration「&」 : expression)
statementvector 是一个类模板,在 C++ 中,我们可以将模板看成是编译器生成类或函数时写的一份说明。
vector 是模板而不是类型,vector是类型。
- vector v1 : 空的 vector 执行默认初始化
- vector v2(v1) : v2 中含有 v1 中所有元素的副本
- vector v2 = v1 : 同上
- vector v3(n, val) : 包含 n 个值为 val 的元素
- vector v4(n) : n个重复地执行了值初始化的对象
- vector v5{a,b,c} : 初始化各个元素
- vector v5 = {a,b,c} : 同上
- 如果要在循环里添加或减少 vector 的元素,则不能使用范围 for 循环。
- 下标运算符只能作用于已经拥有的元素
我们理解一个类型是迭代器当且仅当它支持一套操作,这套操作支持我们能访问容器的元素或者从某个元素引动到另一个元素。
因为模板类都重写了
==和!=运算符,所以多使用!=代替>运算符。
迭代器例子:
vector<int> v4{1,2,3};
for(auto it = v4.begin(); it != v4.end(); ++it)
{
(*it)++;
}
迭代器操作:
- *iter
- iter->item
- ++iter
- 定义数组的长度必须是常量表达式
- 但是实测,如果使用非常量表达式定义数组,数组不会出现异常,但无法写入。
- 使用字面值常量初始化字符数组时会在最后一个位置添加一个空字符,使用列表初始化则不会。
- 指针的数组
int *p[10];- 不存在引用的数组
- 指向数组的指针
int arr[10];
int (*k)[10] = &arr; // 指向长度为10的整型数组的指针。- 数组的引用
int (&p)[10] = arr;- 指向数组的指针的引用(*和变量名在同括号内,就是指针)
int (&*rp)[10] = k; // *&和&*同义-
auto var(数组名)或auto var(&数组名[0])定义一个指针,指向数组首地址。 -
decltype(数组名)表示就是数组类型,并且长度和所限定的数组名相同。 -
推荐使用标准库函数构造迭代器进行遍历
- begin(数组名),必须要是数组名,甚至连指针都不行
- end(数组名)
- 使用指针 + 字面值方式初始化的字符串,是不可写的
- 如果非必须情况,不适用递增递减的后置版本
- int k = {1.2}; 这种语句叫做窄化转换,使用花括号则会出现编译错误
- 条件运算符嵌套不要超过两层
sizeof:返回运算对象所占的字节数- 且指针不需要有效,对指针解引取
sizeof并不会真的获取该数据。 - 返回值是一个常量表达式
- 且指针不需要有效,对指针解引取
- 数组转换成指针
int ia[10];
int *ip = ia;- 指针转换
0或nullptr可以转换成任意指针类型- 任意指向非常量的指针可以转换成
void * - 指向任意对象的指针可以转换成
const void *
格式:cast-name<type>(expression)
cast-name:
- static_cast: 只要不包含底层 const,都可以使用 static_cast。把较大算数类型赋值给一个较小的算术类型时,非常有用。
对于找回指针内的值也很有作用。
void *p = &d;
double *dp = static_cast<double *>(p);- dynamic_cast:
- const_cast:
取消
底层 const属性,但是通过取消后的变量写值是未定义行为。
const char *cp;
char *q = const_cast<char *>(cp);- reinterpret_cast:避免使用,在位模式上重新解释对象
块也是语句,块的内部是零个或多个语句,块不以分号结尾。
如何使得 else 表达成并不是紧跟的 if 的 else?
if(a){
if(b)
...
}
else
doSomething();一般不要省略
case分支最后的break语句。如果没写,最好加上注释。
最好在每个 case 加上花括号,从而保证不会出现跨标签使用未初始化变量的问题。
异常使用 what()成员函数获取c式字符串表示的异常内容。
函数参数尽量使用引用避免拷贝过程,如果不需要改变就是用
const type& name。
当函数需要返回多个数据是,可以将参数设为引用,在函数体内修改引用。
// occurs : 出现的次数
// ret : 第一次出现的索引值
string::size_type find_char(const string &s,char c,string::size_type &occurs)
{
auto ret = s.size();
occurs = 0;
for(decltype(ret) i = 0; i != s.size(); ++i;)
{
if(s[i] == c)
{
if(ret == s.size())
ret = i;
++occurs;
}
}
return ret;
}- 当形参有 const 时,可以用非 const 的变量作为实参传入,但是在函数内他会变成 const 的。
- 不能用
底层 const和const引用初始化非 const 对象。
传递 begin 和 end 参数。
void print(const int* beg, const int* end)
{
while(beg != end){
cout << *beg++ << endl;
}
}如果实参类型相同,使用一个名为initializer_list的标准库模板类型。
如果实参类型不同,使用可变参数模板。
或者省略符,不过一般只用于与C 函数交互的接口程序。
-
initializer_list(实参以包裹花括号的形式传入)
和 vector 一样,有以下操作:
- initializer_list lst;
- initializer_list lst{a,b,c...};
- initializer_list lst2(lst);
- initializer_list lst2 = lst;
- lst.size();
- lst.begin();
- lst.end();
void err_msg(initializer_list<string> il)
{
for(auto beg=il.begin(); beg != il.end(); ++beg){
cout << *beg << " ";
}
cout << endl;
}
err_msg({"134","23123"});不要返回局部对象的引用或指针。
引用返回左值:
char &get_val(string &str, string::size_type ix){
return str[ix];
}
get_val(s,0) = 'A';Type (* function(parameter_list))[dimension]
例如:int (*func(int i))[10];
-
func(int i) : 表示调用函数需要一个 int 实参
-
(*func(int i)) : 表示函数的返回值可以被解引用
-
(*func(int i))[10] : 表示函数的返回值解引用后是一个大小为10的数组
-
int (*func(int i))[10] : 表示数组是 int 类型的
auto function(int i) -> int(*)[10]int odd[] = {1,2,3};
decltype(odd) *function(int i);如果同一作用域内的几个函数名字相同但是形参列表不同,我们称之为重载函数。
底层 const 和 const 引用可以重载(相比于无 const 属于不同类型)。
Record lookup(Account&);
Record lookup(const Account&);最好把默认参数放在后面,这样就可以直接省略不写,否则起不到默认的作用
int temp(int a, int b = 0);
temp(1);
int temp2(int b=0,int a);
temp(2,2);函数体短小且使用频繁的函数建议使用内联。
把
内联函数和constexpr 函数放在头文件内。
assert(expression):assert(0) 则会输出信息并终止程序的执行
通常使用 assert(应该的逻辑)
作为形参,函数和函数指针是等价的。
void a(int (*p)());
void a(int p());三种方式定义函数指针:
int (*f(int))(int *,int);
// 1. f 是个函数,因为有形参int
// 2. f 返回指针因为有 *
// 返回的指针指向的函数类型 外层括号 和 int。auto f(int) -> int(*)(int *,int);int func(int*,int);
decltype(func) *f(int);可以在类内定义别名。
在类外定义函数的时候使用 inline,类内定义会自动 inline。
IO 类是不能拷贝的类型,所以返回 IO 类型是必须以引用的形式,而且 IO对象经常被改变,所以返回的是普通引用。
类内初始值必须用
=或{}初始化。
常量成员函数意味着函数内的 this 被转换成指向常量对象的指针。
当我们创建一个 const 对象时,知道构造函数完成,对象才真正意义上取得其『常量』属性。因此,构造函数可以在 const 对象构造过程中向其写值。 class_name obj_name();这个语句不是执行默认构造函数,而是定义一个函数。 实测:不论什么构造,初始值都会先被赋上。如果不是默认初始化,其他重载的初始化,不手动设置 bool 的成员,那么该成员会被设置为 false,如果是默认初始化,则设为 true。
编译器创造的默认构造函数又被叫做合成的默认构造函数,他的执行规则是:
- 如果存在类内初始值,用它们来初始化成员。
- 否则,默认初始化该成员。
当类包括了内置类型或者符合类型的成员(如指针、数组)时,应当赋予类内初始值,才能被默认初始化,否则结果是未定义的。
该方法定义的某人构造函数和合成的默认构造函数有相同的功能。
className() = default;当有的编译器不支持默认初始值设置的时候,使用构造函数初始化列表来为对象成员赋予初值是一个好的方法。
class_name(int a,int b):member_a(a),member_b(b){}当我们初始化变量以及以值的方式传递或返回一个对象等的时候,会发生对象的拷贝。
total = trans;
// 等价于
total.bookNo = trans.bookNo;
total.units_sold = trans.units_sold;
total.revenue = trans.revenue;一个类如果要允许别的类或函数访问自己的 private 数据,只需要用 friend 声明这个函数或者类,必须在类内声明,位置不限,不受控制符(public、private)的影响。
建议友元声明放在类的开头。这个声明不是通常意义上的声明,如果用户需要使用这个函数,必须再次用正常的方式声明一遍。
class a{
friend int add();
friend class b;
friend void b::function();
public:
}声明为可变成员的变量可以出现在 const 函数或 const 类中,并改变它的值。
const 对象不能用缺省默认构造函数(不写构造函数,或者 class_name() = default;)来构造。
class_name func const(A &b);
class_name func(A &b);每个类负责控制自己的友元类或者友元函数类
类外部需要说明类型名。
// 类外部
Screen::pos ht = 1;
// 类内部(类中的函数内部,这就意味着**外部定义的函数的返回值不在类的内部**)
void Screen::function(pos ht){}
// 类外定义的返回值 出现在 类名之前所以他需要加以标注 说明他属于这个类
Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen&)最好让需要初始化的初始值的顺序和声明时候的顺序一致,如果可能的话避免用一些成员去初始化另一些成员。
// 初始化的顺序是按照成员的顺序来的**(但是看起来好像有的编译器会自动纠正这个错误)**
class X{
int i;
int j;
public:
// 未定义的:i 在 j 之前初始化
X(int val):j(val),i(j){};
};class Z{
int a = 1;
int b = 2;
int c = 3;
public:
Z(int s,int d):a(s),b(d){
a = 1;
b = 2;
}
Z():Z(0,0){
a = 2;
b = 3;
}
};
// 2\3\3(c = 3,是和 a(s),b(d)一起最早初始化的,之后执行 a = 1,b = 2再之后执行 a = 2,b = 3)
Z a;class aa{
int a;
public:
aa(int aaa):a(aaa){
a = 22;
}
void function(aa aa1){
a = aa1.a;
}
};
// 2 隐式转换成 aa(2)
aa ss(2);
ss.function(2);explicit 只允许出现在类内声明处
class aa{
int a;
public:
explicit aa(int aaa):a(aaa){
a = 22;
}
void function(aa aa1){
a = aa1.a;
}
};
// 2 无法隐式转换成 aa(2)
aa ss(2);
ss.function(2);class aa{
int a;
public:
explicit aa(int aaa):a(aaa){
a = 22;
}
void function(aa aa1){
a = aa1.a;
}
};
// 2 无法隐式转换成 aa(2)后拷贝
aa ss = 2;- 所有都是 public
- 没有任何构造函数
- 没有初始值
- 没有基类
- 没有虚函数
那么他可以直接用花括号初始化。
class dd{
public:
int a;
int b;
int c;
};
dd sscs = {1,2,3};- 每个成员都是字面值类型。
- 至少有一个 constexpr 函数。
- 如果有成员初始值,必须是一个常量表达式。
- 使用默认的析构函数。
生成的对象是 constexpr 的。
外部定义静态函数的时候,不要重复添加 static 关键字。
静态成员可以作为实参。
静态成员只能在类外初始化。
对于流内的数据 -> 变量的过程,之前的(空格、tab、换行符)等数据会被抛弃掉(或者指针后移),后面的(空格,tab,换行符等)会被留下来,如果下次 getline 会被读到。
常用条件判断来检测流是否出错
int a = 1;
if (cin >> a){
cout << a;
}
cout << "error";- 函数形参和返回值不能是IO流对象,必须是引用,而且形参不能是const--因为读写都会改变状态。
- 只有支持复制的元素类型可以存储在 vector 或其他容器类型里。由于流对象不能复制,因此不能存储在 vector (或其他)容器中 (即不存在存储流对象的 vector 或其他容器)。
所有的流对象,都包含一个条件状态成员,该成员由setstate和clear操作管理,该成员类型为iostate 。
另外有三个常量,代表三种状态,可进行位操作:badbit、failbit、eofbit。
流的状态检查操作则有:bad、fail、eof、good。如果bad、fail、eof中任一个返回true,则流处于错误状态,否则good操作返回true。
clear和setstate操作用于改变条件成员的状态。clear会重设为有效状态。setstate用于打开某个条件,用于表示某个问题的发生。setstate会保留其他状态。
cin.clear();
cin.ignore(numeric_limits<streamsize>::max(),'\n');
// 不要用 cin.syn()对于 fail 的 cin, 先 clear(除错) 再 ignore(清理掉缓冲区中会继续引发错误的数据) 就可以再一次正常输入了。
cout << flush;
cout << unitbuf; // 切换成无缓冲
cout << nounitbuf; // 切换成正常缓冲缓冲区绑定:当操作输入流之前都会刷新输出流;也可以输出流绑定输出流
// 绑定
cin.tie(&cout);
// 取消绑定
auto ty = cin.tie(nullptr);
cin.tie(ty);可以使用 条件语句 判断流是否是成功的。
文件模式:
- in
- out
- app(追加文件)
- ate(定位到文件末尾)
- trunc(out 状态下默认的,截断文件)
- binary
string test;
while (getline(cin,test)){
istringstream i(test);
string s1;
string s2;
string s3;
i >> s1;
i >> s2;
i >> s3;
int asc = 12;
}原则:
- 除非你有很好的理由选择其他容器,否则应使用 vector。
- 如果你有很多很小的元素,而且额外的空间开销很重要,不要使用 List 或者 forward_List。
- 如果要随机访问,应该使用 vector 或 deque。
- 如果要求在中间插入或删除元素,使用List 或者 forward_List。
- 如果会在头部或尾部插入或删除,但不会从中间插入或删除,则使用 deque。
允许我们从一个不同但相容的类型赋值,祸从一个容器的子序列赋值。assign 使用参数所指定的元素(的拷贝)替换左边容器中的所有元素。
统一使用非成员版本的 swap 是一个好习惯。
交换两个对象的值。
当前元素个数。
使用 emplace_back 替代 push_back。
insert 是在迭代器位置之前插入 vector 不支持 push_front,但是可以用 v.emplace(v.begin,something);
下标索引使用 at 替代 [] ,越界会抛出 out_of_range 异常
c.erase(p) 删除迭代器所在的元素,返回后一个迭代器。如果已经是尾后迭代器,则函数行为未定义。
改变大小,多则删,少则增。
保存尾迭代器是一个坏主意!
- c.shrink_to_fit() // 将capacity()缩小成和 size()一样
- c.capacity() // 不重新分配内存的话,c 可以放多少个元素
- c.reserve(n) // 分配至少能容纳 n 个元素的空间
s.replace(range,arg) // 将 range 范围的内容删掉,添加 arg
- 泛型算法不会检查写操作的合法性。
vector<int> vec;
fill_n(vec.begin(),10,0);返回一个迭代器,这个迭代器不可移动,每次可以作为左值解引。表示向尾部(不论任何情况)添加一个元素,可以自动扩容。
vector<int> a;
auto inserter = back_inserter(a);
*inserter = 1;
a.push_back(3);
*inserter = 2;
// 结果 1 3 2将指定范围的 a1 拷贝给 迭代器a2, 返回 a2 尾元素之后的位置
list<int> b = {5,22,999};
copy(begin(a),end(a),begin(b));返回值 unique后最后一个元素的下一个迭代器:长度不变,后面的数据是什么是不确定的。
一般配合 erase 删除后面的元素。
谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。
// isShorter(二元谓词)
bool isShorter(const string& s1, const string& s2)
{
return s1.size() > s2.size();
}保证按照 isShorter 得出的相同的值元素在容器中的位置不变(在某个基础上,二次排序)。
返回第一个使得谓词非零的元素,否则返回尾迭代器。
可调用对象:可以使用调用运算符(())的对象或者表达式。
有四种,分别是 函数,函数指针,重载了调用运算符的类,lambda 表达式。
lambda 表示一个可以调用的代码单元,我们可以将其理解成一个未命名的内联函数。
它具有一个返回类型,一个参数列表,一个函数体。
但是它与函数不同,他可能定义在函数内部。
[capture list](parameter list) -> return type {function body}capture list是一个lambda所在函数内定义的局部变量的列表。
我们可以省略参数列表和返回类型,但是必须永远包含捕获列表和函数体。
// 引用捕获
auto f = [&k] {return 43;};
// 隐式捕获(值)
auto g = [=,&ks] {return k + ks; //k 是局部变量的拷贝, ks 是引用};
// 隐式捕获(引用)
auto g1 = [&,=k] {return k + ks; // k 是局部变量的拷贝, ks 是引用};
// 使用 lambda 构造多元谓词,但语法结构是一元谓词
auto s = [sd](const int &a){return *sd > a;};
find_if(begin(a),end(a),s);for_each(begin(a),end(a)-10,[](const int &s){cout << s << endl;});
捕获列表里的拷贝值,是定义 lambda 时瞬间变量的值,之后变量改变,不会影响捕获列表里面的值。
如果希望改变拷贝的捕获列表的元素的值,使用 mutable。
int kks = 1;
auto l = [=]{return kks;};
kks++;
auto c= l();bind 函数是一个可以解决传递参数长度问题的适配器。他接收一个可调用的对象,返回一个新的可调用的对象,返回的对象接收的参数个数小于等于原对象。
auto check6 = bind(check_size,_1,6);
// 意味着原来的 check_size 对象被是配成, check6对象,保留了原来的第一个参数(等待调用时输入),第二个参数被设置成6.
check6("23321");
auto g = bind(f,a,b,_2,c,_1);
g(_1,_2); // 映射为 -> f(a,b,_2,c,_1);
// 如果参数内有引用则,ref(val) 或 cref(val)
快速输出容器:
ostream_iterator<int> it(cout," ");
copy(a.begin(),a.end(),it);
cout << endl;map:键值对,set:集合。
map<string,int> word_count;
++word_count["hello"];
--word_count["ss"];
for_each(begin(word_count),end(word_count),[](pair<string,int> s){cout << s.first << " " << s.second << endl;});
// hello 1
// ss -1 set<int> se;
se.insert(1);
se.insert(2);bool func(Sales_data a,Sales_data b){return a > b;}
multiset<Sales_data,decltype(func)*> exmaple(func);
- shared_ptr
- unique_ptr
- weak_ptr
最安全的分配和使用动态内存的方法是调用
make_shared函数,返回一个shared_ptr。
shared_ptr<int> sp = make_shared<int>(4);如果把 shared_ptr 放在容器里,不再需要它的时候记得 erase 掉。
异常如果未在发生的函数内部被捕获,那么函数内部的 shared_ptr 都将被释放
当 p 被销毁的时候执行的函数,
shared_ptr<connection> p(&c, [](Connection *p){disconnect(p);}); auto c = 1;
auto ss = 0;
unique_ptr<int> np(new int(10));
// 转移
unique_ptr<int> next_np(np.release());
unique_ptr<int> temp_np(new int(9));
// 重置
next_np.reset(temp_np.release());先分配,后构造,最后使用。
先销毁,后归还内存。
allocator<string> allo;
// 分配
auto p = allo.allocate(3);
auto q = p;
// 构造
allo.construct(p++,"++");
allo.construct(p++,"--");
allo.construct(p++,"ss");
// 销毁
allo.destroy(p--);
allo.destroy(p--);
allo.destroy(p);
// 收回
allo.deallocate(p,3);拷贝构造函数的一个参数是自身类型的引用,如果有其他参数,其他参数都有默认值。
#include <iostream>
using namespace std;
class a {
private:
int kk = 1;
public:
a() {
cout << "默认构造函数" << endl;
}
a(int k) : kk(k) { cout << "一个参数的构造函数" << endl; }
// 注释即默认的拷贝构造
//a(const a& tem){ cout << "拷贝构造函数" << endl;}
};
int main() {
a b;
a c(22);
a d(c);
a g = c;
a f = 12;
a h = {13};
a i = {a(232)};
return 0;
}如果一个类自定义析构函数,几乎可以肯定它也要自定义拷贝赋值运算符和拷贝构造函数!!
否则,析构时 delete 的掉相同的内容。
class b{
b(const b&) = delete;
b& operator=(const b&) = delete;
// 或者用下面的方法
private:
b(const b&);
};推荐使用 delete 的方式阻止拷贝。
如果一个类定义了自己的拷贝构造函数,编译器就不会为他合成移动构造函数和移动赋值运算符。
int b = 9;
int a&& = b + 1;
// 左值转右值的方法
int a&& = std::move(b);