本文最后更新于: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 协议 ,转载请注明出处!

OOP复习笔记-Part II Previous
Diary: 2020/08 Next