本文最后更新于:2 years ago
Chapter 0: Pre-everything
课程大纲:对象,类,封装,继承,组合,多态,设计模式,模板与STL。
主要Reference: Thinking in C++,second edition,cplusplus.com
History: BCPL -> B -> NB -> C -> C with classes(Bjarne Stroustrup) -> C++(Bjarne Stroustrup)
使用的编译器:g++ 5.4.0, Ubuntu 16.04 LTS
复习中遇到的一些有用函数:
声明/用法 | 函数所在库 | 函数作用 |
---|---|---|
int atoi(const char *str) |
<stdlib.h> | 把字符串转换为一个int 型,转换无效时返回0。 |
cout << right << "" |
这里的right/left用于设置右对齐/左对齐,默认为右对齐,全局有效,设置后要调回来。 | |
cout << setw(20) << |
字段宽度设置,只对紧接着的输出有效,当后面紧跟着的输出字段长度小于n的时候,在该字段前面用空格补齐;当输出字段长度大于n时,全部整体输出。 | |
cout<<setfill('*') |
在上面补齐时,更改默认填充字符。全局有效。 | |
文件输入输出
#include <fstream>
fstream in_file;
in_file.open("ContactEmail.txt",std::ios::in); //std::ios::out
if (!in_file) {
std::cout<<"Error while trying to open the file.\n";
in_file.close();
return 0;
}
in_file.close();
随机:
#include <time.h>
srand(time(NULL));
int a = rand();
计时:
#include <time.h/ctime>
clock_t start = clock();
clock_t end = clock();
double time = (double)(end - start) / CLOCKS_PER_SEC;
0.1 C++11/14/17新特性
0.1.1 特殊的for: range-based for loops
vector int a[6] = {1, 2, 3, 5, 2, 0};
for (auto ia : a) // 相当于 for(int i = 0, ia = a[i]; i < a.size(); i++, ia = a[i])
这需要a是个容器(vector, string等),即a.begin(), a.end()合法。
注意 ia是元素而非迭代器,且是值传递的。
若需要修改a中元素,则应该为for (auto& ia : a)
Chapter 1: 环境
1.1 命令行参数
三种主入口函数的有效声明:
int main(int argc, char** argv)
int main(int argc, char* argv[])
int main()
假设输入为:
./adder.out 2 3 4 5
则接收到的参数为:
argc = 5;
argv = {"./adder.out", "2", "3", "4", "5"};
Note 0: 输入由空格分隔,双引号内可以有空格。
Note 1: Linux的默认工作目录为terminal中所在的工作目录,而非可执行文件所在的目录。
1.2 编译和多文件工程
Translators: 将源代码(source code)翻译为机器指令(machine instructions),分为以下两类:
- Interpreter:边解释边执行,运行期间需要的内存较大,即时反应,效率较低,e.g.:Basic, Perl。
- Compiler:先将源代码全部翻译为机器码,再运行,内存内只存储机器码。需要内存小,效率高。e.g.:C & C++。
一般来说,集成开发环境(IDE)内都会有解释器。
1.2.1 g++编译常用选项
-c: 只编译不链接,只生成目标文件(.o)。
-o:指定生成的可执行文件的名称,如g++ -o a.exe file.cpp
,生成的就会是a.exe
,否则默认生成a.out
。
-g:添加GDB调试选项。
1.2.2 头文件
(1)头文件:用于存放函数、变量、类、模板的声明,后缀为.h, .hpp, .hxx。只声明不定义,否则可能报”Multiple Definitions”的错误。
(2)extern:用于声明函数/变量。
extern int x;
extern void sum(int argc, char* argv[]); //函数情况可以省略
(3)预编译命令(Preprocessor directive):
#include:
<>通常用于寻找系统头文件,寻找的位置为“PATH”所指的 + 编译选项中的(g++ -Idir/to/header)
“”先在当前目录寻找,再去<>找的路径下面找。
Note 1:在函数被定义、使用的地方都包括头文件。
(4)Inclusion Guard:
Guard Macro通常用于防止多次包含头文件的编译错误。在写头文件时一定要写#ifndef
那些。
宏是由preprocessor(预编译器)处理的。
1.2.3 编译链接
解释:将.cpp编译为.o,将.o链接为可执行文件。(TO BE DONE: 头文件在其中哪一步?)
Note:.o也可以被包含后变成.lib/.a/.so。
(1)直接编译链接:g++ main.cpp sum.cpp product.cpp -o a.out
(2)分段编译:仍要链接,分步编译。
Makefile :
# Dependency Rule: 如果冒号后的文件比冒号前的文件新,执行下方的命令
# 如果一个.cpp中有.h,依赖文件中应该有这个.h
# 一定要用缩进!不能使用空格。
a.out: main.o sum.o multiply.o
g++ main.o sum.o multiply.o -o a.out
main.o: main.cpp function.h
g++ -c main.cpp -o main.o
sum.o: sum.cpp function.h
g++ -c sum.cpp -o sum.o
multiply.o: multiply.cpp function.h
g++ -c multiply.cpp -o multiply.o
# 清除所有.o和.out
.PHONY: clean
clean:
-rm a.out main.o sum.o multiply.o
默认target为all
,可以使用make target_name
只生成一个文件。
1.2.4 Linux命令
login # logging in
passwd # change password
logout / exit # logging out
sudo shutdown -h 3 #sudo : run the command as 管理员,shutdown in 3mins
sudo shutdown -h now
sudo shutdown -r now #reboot
sudo reboot #reboot
touch file-name # create an empty file
mkdir dir_name # create an directory
gedit/vi file_name # edit one file with the editor
ls # list contents of the directory
ls --help # for more options
ls -al
#-a 显示所有文件及目录 (ls内定将文件名或目录名称开头为"."的视为隐藏档,不会列出)
#-l 除文件名称外,亦将文件型态、权限、拥有者、文件大小等资讯详细列出
#-r 将文件以相反次序显示(原定依英文字母次序)
#-t 将文件依建立时间之先后次序列出
#-A 同 -a ,但不列出 "." (目前目录) 及 ".." (父目录)
#-F 在列出的文件名称后加一符号;例如可执行档则加 "*", 目录则加 "/"
#-R 若目录下有文件,则以下之文件亦皆依序列出#a:all files including the hidden ones,
# 路径
# ~ : home directory
# ~username: another user's home directory
# .: current dir
# ..: parent dir
# ../.. : parent of parent dir
# pwd: 显示当前绝对路径
# 复制文件
# cp src-file dest-file : copy src to dest
# cp src-file dest-dir : copy src to dest dir
# cp -r src_dir dest-dir: copy src_dir to dest_dir
# 删除文件
# rm file
# rm -r dir
# 压缩文件
# tar zcvf dir.tar.gz dir: pack directory dir
# tar zxvf dir.tar.gz: unpack
# 改变权限
# chmod mode file1 file2 ...
# chmod -R mode dir : change all files in dir
# chmod go-rwx foo.c
# u: user g: group o: other +: add permission
# -: remove permission r: read w:write x:execute
Chapter 2: 类和对象
抽象、封装、继承、多态。
2.1 权限控制
外部只能访问public对象和函数。class的默认权限为private,struct的默认权限为public。
2.2 inline函数
小于10行的函数可定义为inline提高效率,在头文件中声明,不应当包含循环/递归,可以是成员函数。
Note:函数体在类内部定义的函数默认为inline。
2.3 this指针
指向当前对象的地址,在重载运算符时较有用,”return *this”。
2.4 Ctor & Dtor
在对象被创建和销毁时自动由compiler调用,一般都是public。
ClassA a; //在stack上
ClassA *pA = new ClassA; //在heap上
2.4.1 Ctor
没有返回类型,与类同名,可以有参数。
缺省构造函数(default ctor):没有参数的ctor。
默认构造函数(default default ctor):no ctor defined才会由编译器自动生成一个,函数体空。
可以这样使用:
Tree() = default;
使用{}。
关于in-class initializer:
class A
{
public:
int i {5}; // in-class initializer
//事实上会变成A () :i{5} {} ,但可能放在最前面,就被覆盖掉了
A() : i {7} { }
A(int h) : i(h) {}
};
int main()
{
A a;
cout << a.i << endl; //输出7,in-class被初始化列表的覆盖
A b{9};
cout << b.i << endl; //输出9,被强行指定的覆盖
}
成员初始化列表效率高,变量初始化顺序需要与声明顺序相同(不报错的情况下,初始化顺序由声明顺序决定)。
单参ctor可用于所谓implicit conversion,如下:
class Tree{ int height_; public: /*explicit*/ Tree(int h): height_{h} {} }; void foo(Tree t) {} int main() { foo(5); //在不使用explicit关键字的情况下可以通过编译。 }
Delegating Ctor:在初始化列表中使用另一个ctor。
class Tree{ int height_, size_; public: Tree(int h): height_{h} {} Tree() : Tree {0} { size_ = 0 }; //委托 }
2.4.2 Dtor
在(1)变量离开作用域 (2)使用delete关键字 时会被调用。
现代C++中应当用unique_ptr<>和shared_ptr<>替代new & delete。
最好不要显式调用:如pA->~ClassA();
。
- 没有返回值,没有参数,只有一个,不能重载。是public的。
- 在有指针变量/引用变量/socket/printer时一定要有dtor。
- Default dtor: empty,
~Tree() = default;
Note:函数内的静态变量将在第一次调用函数时被construct,main()执行完后被destruct。
2.5 堆和栈
栈(stack):
(0)例:int i; double val; ClassA a;
(1)容量有限;
(2)存储函数的局部变量
(3)会溢出。
堆(heap):
(0)例:int num = 100000; int *arr = new int[num];
(1)大小比栈大,一般不会溢出(大数组定义在这儿)。
Chapter 3: 进阶内容
3.1 宏
基于预编译命令。
定义值:
#define PI 3.14159
。用#undef PI
来取消一个值的定义。guard头文件:
#ifdef
。定义函数:
#define MAX( A, B) ((A) >= (B) ? (A) : (B))
暴力替换,所以最好加括号!在编程中使用:
- _func_ : function name in const char array
- _FILE_ : file name in stirng literal
- _LINE_: current line number in interger literal
- _TIME_: time of the compilation in string literal
- _DATE_: date of the compilation in string literal
DEBUG:
#include <cassert> // important assert(value > 0); //不满足的话会直接终止程序并输出错误
可以用以下方式取消DEBUG,
#define NDEBUG
,g++ -D NDEBUG xxx.cpp
。
Note : 最好不要用宏。宏是由preprocessor处理的,没有进行类型检查,如果不在符号表里记录可能无法debug,函数macro非常易错。倾向于使用compiler,故使用enum/const/inline。
3.2 const
const将默认internal linkage,也即只在被定义的源文件内可见,所以可以在header file内定义。
定义和声明同时。不使用const_cast就不能再改变。
使用extern进行显式声明,且不定义,要写const。
extern const int bufsize;
若size为const的,则
int arr[size];
正确。但如下情况下不行:
class Bunch { const int size; int arr[size]; public: Bunch(int i) : size{i} {} }
因为是先分配空间才执行的初始化。除非改成static const。
普遍用法:
void f1(const int& i);
(没有&的话没有意义)(1)const -> non_const :
const_cast<type>(obj)
, 常量指针/引用变为非常量的指针/引用,指向原来的对象(如obj为const int型,则type处应当为int)(2)non_const -> const : as_const
//const and back again int Class::Function(int a) { // 用常成员函数变一个非-常成员函数出来 return const_cast<ReturnType>(std::as_const(*this).Function(a)); } int Class::Function(int a) const { return xxx; }
//unsafe usage //不应当用于修改constant的值 //可用于:一个不会修改constant值的函数需要一个constant的非-常指针 const int constant = 21; int* modifier = const_cast<int*>(&constant); // *modifier = 22; // 如上语句在某些编译器下将不会修改constant的值,这是未定义的行为
常指针和常值指针:
(1)top-level(常指针):指针不变,对象可变。
ClassA a; ClassA* const pA = &a;
(2)low-level(常值指针):指针变,指向一个常对象。
const ClassA b,c; const ClassA *pB = &b; pB = &c;
OOP相关的:
Note:const只能修饰成员函数而非一般的函数。
常成员函数:不能修改任何成员变量,不能调用任何非-常函数。
void show() const;
常变量:只能在初始化列表里初始化,
常对象:不能修改任何成员变量(任何成员变量不能作为左值),只能调用其常成员函数。
3.3 enum
enum中的值在编译期间都可用,故也可以用来定义数组大小。
#include <iostream> using namespace std; class Bunch { enum { size = 1000, next }; //注意 ; 在 } 之后,默认开始为0 int arr[next]; } int main() { cout << sizeof(Bunch) / sizeof(int) <<endl; }
只能表示整数。
3.4 函数重载
Note 1: 关于引用
double larger(double a, double b);
long& larger(long& a, long& b);
larger(15, 25); //这将调用double版本的函数,因为15,25作为临时变量无法绑定到引用
//但改成const long&后就可以接受临时值了:
long& larger(const long& a, const long& b);
*Note 2 : * const和非const参数只会在low-level指针和引用上有所区分:
int larger(int a,int b) {}
int larger(const int a, const int b) {} //不能通过编译
int larger(int* a,int* b) {}
int larger(const int* a, const int* b) {} //可以,low-level const
int larger(int* a,int* b) {}
int larger(int* const a, int* const b) {} //不能通过编译,top-level const
int larger(int& a,int& b) {}
int larger(const int &a, const int &b) {} //引用,可以
*Note 3: *static_cast
:
static_cast<type-id>( expression )
//将expression转换为type-id,没有运行类型检查来保证转换的安全性
int i = static_cast<int>(166.71);
Base* p = static_cast<Base*>(&derived_obj); //派生类指针转化为基类指针
Note 4: 缺省参数只在声明里给,定义不给(除非你没有声明,那就在定义里给,反正只给一次)。
3.5 const expression
(1)def: 在编译时就计算好的表达式。表达式一定会要求编译器在编译时算出常值。
(2)constexpr function:
- 提示编译器在编译时就从函数中算出常值,编译器不一定会这么做;
- 可以作为ctor的关键字,从ctor中得到常对象。
- 默认inline,应当在头文件里定义。
#include<iostream>
constexpr int arrsize(int i){
return i * i;
}
int main() {
const int i{10}; //事实上这里是int也行
int arr1[arrsize(i)]; //OK
const int a = 2;const int b = 3; //任意去掉一个int就不行了
constexpr int c = a + b;
int arr2[c]; //OK
}
3.6 inline
安全、效率高、有类型检查、可以调试。
将代码复制过去了,所以没有函数调用的成本。适用于<10行、无循环递归、大量调用的简单函数。
仅仅是向编译器发送inline请求,不一定执行。
请定义在头文件内。
可以是普通函数,也可以是成员函数。函数体在类内默认inline。
3.7 namespace
目标:为了防止全局函数、全局变量、类的冲突。
(1)类、结构体、enum、union相当于自动创建命名空间。
(2)用例:可以嵌套,using有效直到达到文件结尾。
namespace u
{
void f() {}
class A {};
} //有没有分号都行,可以嵌套,
int main() {
U::f();
using U::A; //using namespace U;
A a;
return 0;
}
(3)可以在不同/同一头文件中定义同一名字的命名空间。
于是可以将同一命名空间、不同模块的功能划分到不同文件中实现。
//file : test1.h
#include <iostream>
namespace U {
void print1()
{
std::cout << "print1" << std::endl;
}
}
namespace U { //OK
void print2()
{
std::cout << "print2" << std::endl;
}
}
//file : test2.h
#include <iostream>
namespace U {
void print3()
{
std::cout << "print3" << std::endl;
}
}
//file : main.cpp
#include "test1.h"
#include "test2.h"
using namespace U; //将合并所有的U
int main() {
print1();
print2();
print3();
}
3.8 static
3.8.1 static data(变量)
生命期与(一段特定)程序一样长。在一个特殊的静态存储区,不在堆上也不在stack上。
使用internal linkage(static/const/inline),static变量/函数都只在当前编译单元可见。在头文件/源文件定义都行。
- 函数内的静态变量在初次调用函数的时候初始化。
- 全局静态变量在main()函数调用之前初始化。
使用extern关键字可以不包含头文件就使用一个变量/函数,但前提是这个变量/函数并不是internal linkage的(也即不是static/const/inline的),否则编译不能通过。
3.8.2 static成员变量/函数
- 所有该类对象共享,可以通过对象/类名访问。
- 即使在没有对象存在时也存在。
- static关键字只出现在头文件里。
- static成员函数只能访问static成员变量和static成员函数。
3.8.3 Singleton模式:static的应用
class Egg {
private:
static Egg e; //一个private的对象
Egg() {} //ctor声明为private
Egg(const Egg&) = delete;
public:
static Egg* instance() { return &e; }
};
int main() {
Egg e; //doesn't work!
Egg e = *Egg:instance(); //但这个还可以,还需要禁用copy ctor & 赋值
}
3.9 引用
一定要同时定义和初始化,不能改成其它东西的引用。
int &refVal = val;
在形参和返回值中常用。
double average(double (&array)[10]); string& larger(string& s1, string& s2);
不要返回局部变量的引用!
3.10 拷贝构造
Note 1: 不加引用会挂,会引起循环定义(值传递本来就要拷贝)
X(const X obj) {} //WRONG!
X(const X& obj) {} //RIGHT!
新对象定义时必调用其中之一:ctor/copy ctor/move ctor。
Note 2: 用户不定义时会合成一个bitcopy。有指针时会出错。
Note 3: 可以通过声明为private来防止调用。或者使用delete关键字。例如,ofstream对象就禁止了拷贝构造。
A& operator = (const A&) = delete;
A a, b;
a = b; // 编译不通过!
3.11 移动构造
3.11.1 关于左值和右值
左值:可读可写,变量通常都是。例:一个返回引用的函数的返回值。
右值:只可读,例如字面量和临时对象
10, "Hello world" //字面量 int *p, i; //&p 和 i++ 是临时对象 int f() //f()是临时对象
以下用法都是错的:
int a, b; int *x = nullptr; x = &(b + a); //WRONG! x = &123; //WRONG!
- 非常引用只能与左值绑定
- 常引用可以与左值/右值绑定。
3.11.2 右值引用(&&)
仅与右值绑定。
int a = 2, b = 3;
int &&c = (a + b);
cout << c << endl;
3.11.3 移动构造
有copy ctor或者dtor定义的时候就不会自动合成move ctor了。
不会自动摧毁原对象,除非你在move ctor里面定义了。
X(X&& obj); //obj应该不是const的
X a;
X b = move(a); //move将告诉编译器,a应当被视为右值
3.12 结构体的嵌套定义
struct type_1 {
struct type_2 {
struct type_3 {
int data_3[10];
char flag;
} data_2;
float score;
} data_1;
bool is_ok_or_not;
};
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!