The Basics


本系列是C++ Primer的读书笔记

基本内置类型

字面常量

  • 整型和浮点型
    • 0开头的表示8进制,x开头的表示16进制
    • 浮点型字面常量默认是double类型
      • 科学计数法的指数用e或者E表示
        • 3.14159E0
  • 转义字符
    • 换行:\n,回车:\r,退格\b
    • 纵向制表符:\v,横向制表符\t
    • 反斜线:\\
    • 单引号:\', 双引号:\"
    • 进纸符:\f
  • 字符和字符串字面值
前缀 含义 类型 例子
u Unicode 16 char16_t  
U Unicode 32 char32_t  
L 宽字符 wchar_t L'a'
u8 UTF-8(仅用于字符串) char u8"Hi"
  • 整型字面值
后缀 最小匹配类型 例子
u or U unsigned 42ULL //无符号整型字面值,类型是unsigned long long
l or L long 42ULL //无符号整型字面值,类型是unsigned long long
ll or LL longlong 42ULL //无符号整型字面值,类型是unsigned long long
  • 浮点型字面值
后缀 类型 例子
f or F float 1E-3F //单精度浮点型,类型是float
l or L long double 3.14159L,扩展精度浮点型字面值

变量

  • 初始化
    • 变量在定义时被初始化,=不是赋值的意思(赋值是把当前的值擦除)
    • 使用初始化列表(list initialization)
      int x = 0;
      int x = {0};
      int x{0};
      int x(0)
    
    • std::string empty; //非显示初始化一个空串
    • “变量定义尽量初始化”
  • 声明和定义
    • extern int i; // 声明
    • int j; //定义
  • 复合类型(compound type)
    • 左值引用和指针
    • 左值引用定义必须初初始化,初始化对象为另一个变量
      • int &val1 = val2;
      • 引用变量和原变量是同一个地址,是一种binding关系
        int i=0, &r1=i;
        	double d=0, &r2=d;
        	cout<<&i<<endl; //0x7ffee56b1538
        	cout<<&r1<<endl; //0x7ffee56b1538
      
  • const
    • 定义const对象必须初始化
    • 默认情况下const对象只在当前文件内有效,如果要在不同文件中共享const,在头文件中添加声明,在.c文件中定义
      • .h文件中声明:extern const int buff;
      • .c文件中定义:extern const int bufSize = fcn();
    • 如果用const定义指针,
      • 顶层(top-level)`const`指的是指针本身是常量
      • 底层(low-level)`const`指的是这个指针是一个指向常量的指针
        int i=0;
        int *const p1 = &i; //顶层const,p1的值不能改变,可以改变它指向的值
        const int ci = 42;	//顶层const
        const int* p2 = &ci; //底层const,允许改变p2的值
      
      • 常量表达式,constexpr ,如果某个const变量的值在编译时就能确定,可以将其定义为常量表达式
        const int max = 20; //是常量表达式
        const int limit = max+1; //是常量表达式
        int sz = 29; //不是
        const int buff_size = get_size()//不是,因为buff_size的值要在运行时决定
      
      • 如果是常量表达式,可以用constexpr来定义变量,而且必须用常量表达式来初始化
        constexpr int mf = 20;	//20是常量表达式
        constexpr int limit = mf+1; //mf+1是常量表达式
        constexpr int sz = size(); //只有当size()是一个constexpr函数时,才正确
      
      • 如果用constexpr定义指针,要注意,得到的指针是一个常量指针,初始值必须要能在编译时确定
        constexpr int *p = nullptr; //定义了一个指向整数的常量指针,值为0
        const int *q = nullptr; //定义了一个指向整型的常量指针,注意区别
      
  • type alias
    • using :类似typedef
      • using SI = sales_item; SI item;
  • auto
    • C++11新的类型说明作符,让编译器推断变量类型,因此使用auto定义的变量必须要有初值
    • auto item = val1 + val2;
    • 使用auto要注意const的情况
      const int i=100;
      auto *p = &i;
      *p = 111; //error, i is a real-only 
    
  • decltype
    • C++11新的类型说明符,它的作用是选择并返回表达式的数据类型,编译器只做类型推断,不进行表达式求解
      decltype(f()) sum x; //编译器并不实际调用f
    
    • 如果decltype中的表达式是指针取值操作,得到的类型是引用类型
      int i = 42;
      int *p = &i;
      decltype(*p) c; //错误,decltype(*p)返回的结果是int&,因此必须赋初值
    
    • 如果decltype后面的表达式加上了一对括号,返回的结果是引用
      decltype((i)) d; //错误: d是int&, 必须初始化
      decltype(i) d; //正确; d是int型变量
    
  • typeid

C++11中可以使用typeid得到符号的混淆(mangling)结果

int main(){
	string s;
	cout<<typeid(s).name()<<endl; //NSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE
}

字符串,向量,数组

头文件

  • 原C中的库函数文件定义形式为name.h,在C++中统一使用<cname>进行替换,去掉了.h,在前面增加字母c。在cname中定义的函数从属于标准库命名空间std

  • 尽量不要在头文件中不包含using声明

stirng

string类是一个模板类,它的定义如下:typedef basic_string<char> string;

  • 初始化
stirng s1; //默认初始化,s1是空串
string s2(s1); //拷贝初始化
string s3=s1; //拷贝初始化
string s4("value"); //拷贝常量字符串
string s5 = "value"; //和上面相同
string s6(10,'c'); //重复c十次
//从 const char* 初始化
const char* cp = "Hello World!!!";
string s7(cp);
  • 子串
substr(pos,n) 返回一个string,包含s中从pos开始的n个字符的拷贝。pos默认值为0,n的默认值为s.size()-pos即拷贝从pos开始的所有字符
string s("hello world");
string s2 = s.substr(0, 5); // s2 = hello
string s3 = s.substr(6); // s3 = world
string s4 = s.substr(6, 11); // s3 = world
string s5 = s.substr(12); // throws an out_of_range exception
  • 修改字符串
s.insert(pos, str) 在pos的位置之前插入str,返回s的引用
s.erase(pos,len) 删除从pos开始len长度的字符,如果len被省略,则删除从pos开始直到末尾的全部字符
s.append(str) 追加str在末尾,也可以使用+ str
s.replace(pos, len, str) 从pos位置开始,向后删除len个字符,并在删除位置插入str
string s("C++ Primer"), s2 = s; // initialize s and s2 to "C++ Primer"
s.insert(s.size(), " 4th Ed."); // s == "C++ Primer 4th Ed."
s.erase(11, 3); // s == "C++ Primer Ed."
s.insert(11, "5th"); // s == "C++ Primer 5th Ed."
s.replace(11, 3, "Fifth"); // s == "C++ Primer Fifth Ed."
  • 搜索子串

string提供了6个不同的搜索函数,每个搜索函数有4个重载版本。每个搜索操作都返回string::size_type类型,表示匹配的下标结果,如果搜索失败,返回string::npos的static成员,npos的类型为const string::size_type,值为-1

s.find(str) 返回str首次出现的位置
s.rfind(str) 反向查找,返回str最后一次出现的位置
s.find_first_of(str) 在s中查找str中任意一个字符第一次出现的位置
s.find_last_of(str) 反向查找,在s中查找str中任意一个字符最后一次出现的位置
s.find_first_not_of(str) 在s中查找第一个不在str中字符的位置
s.find_last_not_of(str) 反向查找,在s中查找最后一个不在str中字符的位置
string name("AnnaBelle");
auto pos1 = name.find("Anna");  // pos1 == 0
string lowercase("annabelle");
pos1 = lowercase.find("Anna");   // pos1 == npos

// returns 1, 在name中找到第一个数字的index
string numbers("0123456789"), name("r2d2");
auto pos = name.find_first_of(numbers); //returns 1
//  找到第一个不是数字的字符位置
string dept("03714p3");
auto pos = dept.find_first_not_of(numbers); //returns 5,
  • 数值转换
to_string(val) 一组重载函数,返回数值val的string表示
stoi(s,p,b) 返回s对应数值,p默认值为10, b默认是0
stol(s,p,b)  
stoul(s,p,b)  
stoll(s,p,b)  
stoull(s,p,b)  
stod(s,p,b)  
stof(s,p,b)  
stold(s,p,b)  

vector

  • 初始化

可使用值初始化,和初始化列表,当编译器确认无法使用初始化列表时,会将花括号中的内容作为已有的构造函数参数

vector<int> a(10); //使用值初始化,创建10个元素的容器
vector<int> a{1,2,3} //使用初始化列表,创建一个容器,前三个元素是1,2,3

//上述代码等价于:
initializer_list<int> list = {1,2,3};        
vector<int> v(list);

vector<string> a{"ab","cd"} //使用初始化列表,创建一个string容器,并初始化前两个元素
vector<string> a{10}; //a是一个默认有10个初始化元素的容器,类型不同,不是初始化列表,退化为值初始化
vector<string> a{10,"hi"}; //a是一个默认有10个初始化元素的容器,类型不同,不是初始化列表,退化为值初始化
  • 内存增长

vector内部是连续存储,尾部push效率高,insert操作会很低效,需要重新分配空间并移动元素。因此vector在内部实现上会做某些优化来减少对内存的频繁操作,其策略是预先分配大于size的空间。可以通过capacityreserve来干预内存分配

c.shrink_to_fit() capacity()减少为和size()相同大小
c.capacity() 不重新分配空间的前提下,能存储的最大元素个数
c.reserve(n) 手动指定vector大小,分配至少能容纳n个元素的空间,在执行reserve(n)后,capacity()返回的值应该大于等于n
vector<int> vc;
cout<<"ivec: size: "<<vc.size()<<" capacity: "<<vc.capacity()<<endl;
for(vector<int>::size_type ix = 0; ix != 24; ++ix){
	vc.push_back(ix);
}
cout<<"ivec: size: "<<vc.size()<<" capacity: "<<vc.capacity()<<endl;

// ivec: size: 0 capacity: 0
// ivec: size: 24 capacity: 32
  • 使用迭代器

C++11中,不论集合对象是否是const的,使用cbegin可以返回常量迭代器

vector<int>::iterator it;
vector<string>::iterator it2;
//只读迭代器
vector<int>::const_iterator it3;
vector<string>::const_iterator it4;

//如果记不住迭代器类型,可以使用auto自动推导
auto b = v.begin(); //b表示v的第一个元素
auto e = v.end(); //e表示v的最后一个元素
auto it1 = v.cbegin(); //
auto it2 = v.cend(); //cend同理
  • 迭代器操作
*iter
iter -> mem //等价于 (*iter).mem
++iter
--iter
iter1 == iter2
iter1 != iter2

数组

  • C++11新增标准库函数beginend,用来返回静态数组的头指针和尾指针
int a[] = {1,2,3,4,5,6};
int *pbeg = std::begin(a);
int *pend = std::end(a);

遍历数组无需知道数组长度,有头尾指针即可

whlile(pbeg!=pend){
	//do sth..
	pbeg++;
}
  • 使用数组来初始化vector,由于数组存储是连续的,因此只要指明这片存储空间的首尾地址即可
int arr[] = {1,2,3,4,5}l
vector<int> vc(begin(arr), end(arr));

表达式

基础

  • 运算符
    • 一元运算符:作用于一个对象的运算符,如&,*
    • 二元运算符:作用于两个对象的运算符,如=,==,+
  • 左值
    • 当一个对象被用作左值的时候,用的是对象的身份(在内存中的位置),因此左值有名字
    • 左值是定义的变量,可以被赋值
    • 如果函数的返回值是引用,那么这个返回值是左值
  • 右值
    • 当一个对象被用作右值的时候,用的是对象的值(内容),因此右值没有名字
    • 右值是临时变量,不能被赋值
    • 如果函数的返回值是数值,那么这个返回值是右值

递增和递减运算符

  • ++i: 将i先+1后,作为左值返回,返回的还是i本身
  • i++: 先将i的拷贝作为右值返回,然后执行i+1
  • 除非必须,否则不用后置版本

语句

迭代语句

  • 范围for循环
for(declaration: expression){
	statement
}

C++11提供了这种简便的for循环语句

vector<int> v={1,2,3}
for(auto &r : v){ //使用auto来处理类型
	//注意,引用会修改原对象
	r *= 2;
}

异常处理

  • 使用throw抛出异常
if(a!=b){
	throw(8); //throw 一个int型的异常
	throw("exception"); //throw 一个string型的异常
	throw runtime_error("runtime error"); //throw 一个runtime error
}
  • 使用try-catch捕获异常
bool err1 = true, err2 = true;
try{
	if(err1){
		if(err2){
			throw runtime_error("runtime error!");//向上传递,会被最外层catch
		}
	}else{
		throw string("err1");
		// throw 8;
	}
}catch(string exc){
	cout<<exc<<endl;
}catch(int exc){
	cout<<exc<<endl;
}catch (runtime_error err){ 
	cout<<err.what()<<endl;
}

如果是非内核的错误,catch到后程序仍可继续运行

  • 标准异常
    • exception类,头文件<exception>
      class exception {
          public:
          exception () throw();
          exception (const exception&) throw();
          exception& operator= (const exception&) throw();
          virtual ~exception() throw();
          virtual const char* what() const throw();
          }
    
    • <stdexception>中定义了几种常用的异常类继承自exception
      • runtime_error : 运行时异常
      • range_error: 运行时错误,生成的结果超出了有意义的值域范围
      • overflow_error: 运行时错误,计算上溢
      • underflow_error: 运行时错误,计算下溢
      • logic_error: 逻辑错误
    • new的头文件定义了bad_alloc类型的异常
      int main(){
          try{
              char *p = new char[99999999999999999];
              delete[] p;
          }
          catch (bad_alloc e){
              cout <<"catch error:"<< e.what() << endl;
          }
      }
    
    • type_info头文件定义了bad_cast异常类型
  • 自定义异常类型
class MyException: public exception{
public:
     virtual const char* what() const throw(){// const throw 意思是这个函数不会抛出异常
        return "MyExcepiton!";
     }
};

void throwException(){
    throw MyException();
}
void throwException() throw(){ //声明这个函数不会抛异常
    throw MyException(); //不会抛异常而是直接出错
}

int main()
{
    try{
        throwException();
    }catch(MyException &e){
        cout<<e.what()<<endl;
    }
}

函数

参数传递

  • 传引用
  • 传值(pass by value)
    • 基本数据类型直接拷贝
    • 指针变量也是拷贝
  • 使用常量引用
    • 如果参数中有引用类型,将其声明为const
  • 数组做函数形参
    • 遵循两个规则:
      1. 不允许拷贝数组
      2. 使用数组做参数时,传递数组名做指针指向数组首地址
        • void print(const int *) //三者等价
        • void print(const int[]) //只读数组,形参声明为const
        • void print(const int[10])
      3. 上述定义可以注意到:int *int []定义等价
    • 数组引用
      • 引用形参绑定到对应的实参上
      • void print(int (&arr)[10]) //注意参数名两侧括号
    • 多维数组
      • void print(int (*matrix)[10]) //指向含有10个整数的数组指针
      • void print(int matrix[][10], int rowsize) //等价定义
    • main函数命令行选项
      • main(int argc, char* argv[])
        • 第二个形参argv是一个数组,它的元素是字符串的指针
        • 由于指针和数组名等价,也可将其用指针表示
          • main(int argc, char **argv) //argv指向char*
        • argv的第一个元素为程序名称,有意义的参数从argv[1]开始
          argv[0] = "prog"; //程序名称
          argv[1] = "-d";
          argv[2] = "-o";
          argv[3] = "ofile";
          argv[4] = "data0";
        
  • 可变参数
    • 使用可变参数模板
    • 使用initializer_list<T>
      • 无法修改参数列表中的值
      • 有迭代器
        void err_msg(initializer_list<string> params){
        	for(auto beg = params.begin(); beg!=params.end();++beg){
        	cout<<*beg<<endl;
        	}}
        err_msg({"function x","88"}); //使用{...}构造
        err_msg({"function x","88","100"});
      

返回值

  • 返回引用的函数,返回值可作为左值;其它返回类型,返回值为右值
  • main函数的返回值:
    • 如果函数的返回值类型不是void,那么函数必须有一个返回值,main函数是例外,如果控制到达了main函数的结尾,没有return语句,编译器会插入return 0
  • 数组不能拷贝,因此函数不能返回数组,但可以返回一个指向数组的指针
int arr[10]; //arr是一个含有10个整数的数组
int *p1[10]; //p1是一个含有10个指针的数组
int (*p2)[10];  //p2是一个指向数组的指针,这个数组的每个元素是一个含有10个元素的数组

给出返回数组指针的函数定义为:

Type (*function(parameter_list))[dimension]

上面式子中Type为元素类型,dimension为数组大小,例如:

int (*func(int i))[10];

它的含义如下:

  1. func(int i)表示调用func函数时需要一个int型的实参
  2. (*func(int i))表示对函数调用结果进行解引用操作
  3. (*func(int i))[10]表示解引用操作返回的是一个有10个元素的数组
  4. int (*func(int i))[10]表示该数组的类型是int

尾返回类型(tailing return type)

  • C++11提供尾返回类型,符号为->
  • 对返回值是复杂的类型的函数可以用lambda表达式定义:
//int (*func(int i))[10]的lambda表达式写法
auto func(int i)->int(*)[10]

使用decltype

对上面的情况无论是原生写法还是lambda表达式都不是很直观,个人认为使用decltype是个不错的选择,例如:

int even[] = {0,2,4,6,8};
int odd[]  = {1,3,5,7,9};
//返回一个指针,该指针指向含有5个整数的数组
decltype(odd) *arrPtr(int i){
	return (i%2==0)?&even:&odd; //返回一个指向数组的指针
}

使用decltype(odd)得到了一个数组类型,要返回指向数组的指针,因此后面要加上*

重载(Overload)

如果同一作用域内的几个函数,名字相同,但形参列表不同,则称之为函数重载。在函数调用时,编译器会根据实参类型确定调用哪个函数。

C++允许函数重载

int max(int a,int b, int c);
int max(double f1, double f2);

上面两条函数声明在C++中不会报错,但是在C中因为函数的符号相同会报错

int max(double a,double b);
void max(double f1, double f2); //error

上面两条函数声明会报错,因为仅返回值类型不同不属于重载,属于方法的重复定义。对于函数重载,必须要要求返回值类型相同

const形参

当形参中有const修饰时,如果是顶层const,则不算做重载,因此下面两个函数声明不属于重载,它们是等价的:

Record lookup(Phone x);
Record lookup(const Phone x); //重复声明

Record 

但是如果是底层const,用来修饰指针或者引用,则算作重载

Record lookup(Account& ); //函数作用于Account的引用
Record lookup(const Account& ); //新函数,作用于常量引用

Record lookup(Account* ); //函数作用于指向Account的指针
Record lookup(const Account* ); //新函数,作用于指向Account常量的指针

使用const-cast可以实现变量在const和非const之间转换

string &s1 = some_string;
const string& c_s1 = const_cast<const string&>(s1); //s1从non-const转到了const
string &s2 = const_const<string&>(c_s1); //c_s1从const转换成了non-const 

缺省函数

void func(int x, int y=1, int z=2){}
func(10); 
func(10,2);
func(10,,9);//error

尽量在函数声明时指定形参的默认值

constexpr函数

C++11提供了一种constexpr函数,它的目的是用于常量表达式。它要求函数的返回值,形参都是常量,函数体只能有一个return语句。其目的是让编译器在编译的时候即可将函数展开,替换其执行结果:

constexpr int new_size() { return 42; }
constexpr int foo = new_size(); //正确,foo是一个常量表达式

constexpr的返回值可以不一定是常量,只要编译器在编译时能自动推断即可:

constexpr size_t scale(size_t cnt) {
	return new_size()*cnt;
}

int arr[scale(2)]; //正确,scale(2)是常量表达式
int i=2;
int arr2[scale(i)]; //错误,i无法再编译器推断,scale(i)不是常量表达式

对于constexpr函数和inline函数,编译器要展开它们的内容仅有声明时不够的,因此它们通常定义在头文件内

函数调试

  • 使用assert
    • 头文件:<cassert>
    • 定义:assert(expr)是一个预处理宏定义
      • expr求值返回0,则assert报错,终止程序
      • expr求值返回非0,assert什么也不做
        assert(word.size() > threshold); //报错
      
  • 使用NDEBUG

assert的开关,如果定义了NDEBUG,则assert失效。也可以作为调试开关

void print(const int ia[], size_t size){
	#ifndef NDEBUGE
		cerr<<__func__<<": array_size is "<< size<<endl;
	#endif
}

函数指针

  • 定义: Type (*func) (paramerter_list ):
bool lengthCompare(const string& , const string &);

它的类型为:bool(const string&, const string&),函数指针只需要补充上函数名:

bool (*fp) (const string&, const string&);
  • 当把函数名作为值使用时,该函数名自动转为指针
pf = lengthCompare; //pf指向lengthCompare函数
pf = &lengthCompare; //等价语句,取地址符号可选

bool b1= pf("hello","goodbye");  //调用lengthCompar函数
bool b2 = (*pf)("hello","goodbye"); //等价调用
  • 用作函数的形参
//第三个参数是函数类型,编译器会自动将其转成函数指针
void useBigger(const string& s1, const string& s2, bool pf(const string s1&, const& str));
//等价声明,显式的将形参声明称函数指针
void useBigger(const string& s1, const string& s2, bool (*pf)(const string s1&, const& str));

上面声明太过复杂,可以使用typedefdecltype简化

//Func和Func2是函数类型
typedef bool Func(const string&, const string&);
typedef decltype(compareLength) Func2; //等价类型

//Funcp和Funcp2是函数指针
typedef bool (*Funcp)(const string&, const string&);
typedef decltype(compareLength) *Funcp2; //等价类型

注意:函数类型和函数指针类型不一样

  • 用作函数的返回值
using F = int(int*, int*); //F是函数的类型
using FP = int(*)(int*, int*); //FP是函数指针

F* f1(int);
FP f1(int);

然而我们也可以直接声明f1:

int (*f1(int))(int,int);

从内向外读:

  1. f1有形参列表,说明f1是一个函数
  2. f1左边有*说明它的返回值是指针
  3. 指针也有形参列表,说明返回的是一个函数指针
  • 使用尾返回类型表示
auto f1(int) => int(*)(int,int);
  • 使用decltype

当有两个函数签名一致的时候,我们可以写一个函数在运行时根据条件返回这两个函数中的任意一个

string::size_type sumLength(const string& s1, const string& s2);
string::size_type largerLength(const string& s1, const string& s2);

//根据形参值,来返回sumLength或者largeLength
decltype(sumLength) *getFcn(const string& );

注意,decltype(sumLength)返回的是函数类型,因此需要加上*,表明返回的是指针。

  • 关键字classstruct
    • 唯一区别是默认的访问权限
      • 使用struct, 访问说明符之前的成员都是public的;使用class这些成员是private的

构造函数

  • 对象不论以什么样的形式创建都会调用构造函数
  • 成员函数的一种
    • 名字与类名相同,可以有参数,不能有返回值
      • 作用是对对象进行初始化,给成员变量赋值
      • 如果没定义构造函数,编译器生成一个默认的无参数的构造函数
  • 默认构造函数
    • 当类没有自定义构造函数时,编译器会默认生成一个构造函数(synthesized default constructor),
      • 当声明了自定义构造函数时,编译器不会提供默认的构造函数,需要自己制定
      • C++ 11可以在参数列表后面使用=default来要求编译器生成默认构造函数
        struct Sales_data{
            Sales_data() = default;
            Sales_data(const string &isbn);
            ...
        }
      
    • 使用默认构造函数
      Sales_data obj;
      Sales_data obj();  //wrong!,定义了一个函数
    
  • 拷贝构造函数
    • X::X(X& x), 一定是该类对象的引用
    • X::X(const X& x) ,一定是该类对象的引用
    • 三种情况会调用拷贝构造函数
      • 用一个对象去初始化同类的另一个对象
        Complex c1(c2); 
        Complex c1 = c2; //调用拷贝构造函数,非赋值,Complex c2; c2 = c1; //这是赋值
      
      • 函数传参时,如果函数参数是类A的对象,则传参的时候会调用拷贝构造
        void func(A a){ ... }
        int main(){
            A a2;
            func(a2)
        }
      
      • 类A做函数返回值时,会调用拷贝构造
        A func(int x){
            A b(x);
            return b;
        }
        int main(){
            A a1 = func(4);
        }
      
    • 如果没有定义拷贝构造函数,则系统默认生成一个
    • 拷贝构造函数,如果涉及成员变量指向一片内存空间的,需要使用深拷贝,赋值被拷贝对象的内存空间
  • 类型转换构造函数
    • 如果构造函数只有一个参数,它实际上定义了隐式的类类型转换机制
    • 不是拷贝构造函数
    • explicit关键字
      • 如果使用explicit关键字声明了该构造函数,则不能使用隐式初始化
      • explicity仅对有一个实参的构造函数起作用
      class B{
      public:
          int m;
          string s;
          B (int x):m(x){
              cout<<m<<endl;
          };
          explicit B(string ss):s(ss){
              cout<<s<<endl;
          }
      };
      B b1 = 10; // 相当于 B tmp(10); B b1 = tmp;
      B b2 = "abc"; // Wrong!
    
  • 委托构造函数
    • C++ 11提供了委托构造函数,该类构造函数可以调用其它构造函数
      class Sales_data{
          public:
              Sales_data(string s, unsigned cnt, double price){
                  //...
              }
              Sales_data():Sales_data("",0,0){ //... } //调用第一个构造函数
              Sales_data(string s): Sales_data(s,0,0){ //... } //调用第一个构造函数
      }
    

友元

  • 类允许其他类或者函数访问他的非公有成员,方法是令其他类或者函数成为它的友员
    • 友元函数
    • 友元类
  • 友员属性不可传递和继承
class Car
{
private:
	int price;
public:
	Car(int p):price(p){}

//友元函数声明,允许这个函数访问`price`成员
friend int mostExpensiveCar(Car* pCar);
//友元类声明,允许这个类访问私有成员
friend class Driver;
};

//只要函数签名能对上就可以访问
int mostExpensiveCar(Car* pCar){
	//访问car的私有成员
	printf("Car.price:%d\n",pCar->price);
};

class Driver{
public:
	void getCarPrice(Car* pCar){ //Driver是Car的友元类,可以访问其私有成员
		printf("%s_Car.price:%d\n",__FUNCTION__,pCar->price);
	};
};

析构函数

类成员

  • 使用typedef或者using
class Screen(){
	typedef std::string::size_type pos1;
	using pos2 = std::string::size_type
};
  • 可变数据成员
    • 使用immutable关键字声明某个成员,允许它在const成员函数内被修改
      class Screen(){
          public: 
              void some_member() const;
          private:
              mutable size_t access_str; //即使在const函数内也可以被修改
      };
      void Screen::some_member() const{
          ++access_str; 
      }
    
  • 类内初始化 C++ 11支持成员在类内声明时赋初值

  • 封闭类
    • 一个类的成员变量是另一个类对象,包含成员对象的类叫封闭类
      class Car{
      private:
          int price;
          Engine engine;
      public:
          Car(int a, int b, int c);
      };
      //初始化列表
      Car::Car(int a, int b, int c):price(a),engine(b,c){};
      //这种情况,Car类必须要定义构造函数来初始化engine
      //如果使用默认构造函数,编译器无法知道Engine类对象该如何初始化。
    

类成员函数

  • 内联成员函数
    • 定义在类内部的成员函数是自动`inline`的
    • 显式使用inline关键字的声明
      • 可在类内部,也可在外部定义时声明
      class B{
          inline void func1(); //显式inline
          void func2(){...} //隐式inline
          void func3();
      };
      void B::func1(){...}
      inline void B::func3(){...} //也可以在函数定义处inline
    
  • 成员函数支持重载
class A{
	int value(int x){ return x;}
	void value(){ }
}
  • 静态成员函数
    • 相当于类方法,不作用于某个对象,本质上是全局函数
    • 不能访问非静态成员变量
    • 不能使用this指针,它不作用于某个对象,因此静态成员函数就是c语言的全局函数,没有多余的参数。
    • 访问:
      • 使用类名访问:类名::成员名: CRectangle::PrintTotal();
      • 使用类对象访问:对象名.成员名: CRectangle r; r.PrintTotal();
  • const成员函数
    • `const`成员函数不能修改成员变量,不能访问成员函数
      • 本质上是修改了this指针的类型
        struct Sales_data{
      			
            //默认情况下`this`的指针类型为`Sales_data* const`,是一个常量指针。
            //如果要求它不修改成员变量,则必须要改变`this`的类型为指向常量的指针,即`const Sales_data* const`
            std::string isbn() const{
                return this->bookNo;
            }
        }
      
      • 实际意义是不允许该函数修改对象的任何状态
    • const成员函数也可作为构造函数,算重载
      class Hello{
      private:
          int value;
      public:
              void getValue() const;
              void foo(){}
      };
      void Hello::getValue() const{
          value = 0;//wrong;
          foo(); //error
      }
      int main(){
          const Hello o;	
          o.value = 100; //wrong!
          o.getValue(); //ok
          return 0;
      } 
    

类对象

  • 常量对象
    • 常量对象不能修改成员成员变量,不能访问成员函数
      class Demo(){
          public:
              Demo(){} //如果有常量对象,则必须要提供构造函数
              int x;
              void func(){}; //虽然func没有修改操作,但是编译器无法识别
      };
      int main(){
          const Demo c;
          c.x = 100; //wrong!
          c.func(); //wrong
      }
    
  • 使用对象引用作为函数的参数
class Sample{ ... };
void printSample(Sample& o)
{
	...
}

对象引用作为函数参数有一定风险,如果函数中不小心修改了o,是我们不想看到的。解决方法是将函数参数声明为const,这样就确保了o的值不会被修改

void printSample(const Sample& o)

this指针

  • this是一个常量指针,地址不可修改,在早期c++刚出来时,没有编译器支持,因此需要将c++翻译成c执行,例如下面一段程序:
void Car::setPrice(int p){
	price = p;
}
//编译器展开
void setPrice(struct Car* this, int p){
	this -> price = p;
}
  • class对应struct
  • 成员函数对应全局函数,参数多一个this
  • 所有的C++代码都可以看做先翻译成C后在编译

理解下面一段代码:

class Hello
{
	public:
		void hello(){printf("hello!\n");};
};
int main(int argc, char** argv)
{
	Hello* p = NULL;
	p -> hello();
}

程序会正常输出hello,原因是成员函数void hello()会被编译器处理为:void hello(Hello* this){printf("hello!\n");}与this是否为NULL没关系。

聚合类(Aggregate class)

聚合类满足以下几个条件:

  1. 所有成员都是public
  2. 没有定义任何构造函数
  3. 没有类内初始值
  4. 没有基类,也没有virtual函数
struct Data{
	int ival;
	string s;
};
//使用聚合初始化函数
Data val1 = {0, "Amna"};

类的静态成员

  • 该类的所有对象共享这个变量,是全局变量
  • sizeof运算符不会计算静态成员变量
  • 静态成员可以在类内部定义,但是只能在类外部初始化,constexpr类型的静态成员除外。
class B{
private:
	static constexpr int period = 30;
public:
	static void printVal();
	static int val; //类内部定义静态成员
	double amount;
	void calculate(){
		amount += val; //成员函数内部不需要通过类作用域符号访问静态成员
	}
};
int B::val = 0; //类外部初始化
void B::printVal(){
	cout<<__FUNCTION__<<"L "<<B::val<<endl;
}
constexpr int B::period; //即使一个常量静态数据成员在类内部已经初始化了,最好也要在类外部定义一次

//
int x = B::val;
B::printVal();
B b;
b.val;
b.printVal();

IO库

IO类

  • 标准库中三个类
    • iostream定义了用于读写流的基本类型
      • istream,wistream 从流读取数据 — ostream,wostream 向流写入数据
      • iostream,wiostream 读写流
    • fstream定义了读写文件的类型
      • ifstream : 从文件中读数据
      • ofstream: 向文件中写数据
      • fstream: 文件读写流
    • sstream定义了读写内存string对象的类型
      • istringstream
      • ostreamstream
      • stringstream
  • 继承关系如下:

  • 标准流对象:
    • cin标准输入流,用于从键盘读取数据,也可以被重定向为从文件中读取数据
    • cout对应标准输出流,用于向屏幕输出数据,也可以被重定向为向文件写入数据
    • cerr对应标准错误处输出流,用于向屏幕输出错误信息
    • clog对应标准错误输出流,用于向屏幕输出出错信息
    • cerrclog的区别在于cerr不适用缓冲区,而输出到clog中的信息会先被存放到缓冲区,缓冲区满了才刷新到屏幕
#include <iostream>
using namepsace std;
int main(){
	//输出重定向
	int x,y;
	cin >> x>>y;
	freopen("test.txt","w",stdout); //将标准输出重定向到test.txt中
	if(y == 0){
		cerr<<"error"<<endl; //cout被重定向到文件,调试信息可使用cerr输出
	}else{
		cout << x/y; 将结果输出到test.txt
	}
	
	//输入重定向
	double f; int n;
	freopen("t.txt","r",stdin); //cin被改为从t.txt中读取数据
	cin>>f>>n; //t.txt中的文件存放格式要符合cin的要求, t.txt: 3.14 123
	cout<<f<<","<<n<<endl; //3.14,123
	return 0;
}
  • 向文件中写入struct

文件操作

  • 简单读写文件
fstream fout("text.txt",ios::out | ios::binary);//使用基类fstream,需要指明读写类型
//或者使用ofstream
//ostream fout("text.txt");
if(fout.is_open()){
	//使用流操作符
	fout<<"Hello there"<<endl;
	fout<<123<<endl;
	fout.close();
}else{
	cout<<"Could not create file: text.txt"<<endl; 
}

//读文件
ifstream fin("text.txt");
if(fin.is_open()){
	string line;
	//使用getline
	while(!fin.eof()){
		getline(fin, line);
		cout<<line<<endl;
		fin>>ws; //抛弃掉line末尾的换行
		if(!input){
			break;
		}
	}
	fin.close();
}else{
	cout<<"Could not read file: text.txt"<<endl;
}
  • 读写结构体
    • 由于编译器有内存对齐的优化,因此struct实际的size可能会变大,所以需要要先压缩结构体
      #pragma pack(push, 1)
      struct Person{
          char name[50]; //50 bytes
          int x; //4 bytes
          double p;//8 bytes
      };
      #pragma pack(pop) //结构体的size从64 bytes变成62bytes
    
      int main(){
          //写二进制文件
          Person someone{"Frodo",220, 0.8};
          ofstream fout{"test.bin",ios::binary}; //二进制文件
          fout.write(reinterpret_cast<char*>(&someone), sizeof(Person));
          fout.close();
    
          //读二进制文件
           Person frodo = {};
          ifstream fin{"test.bin",ios::binary};
          fin.read(reinterpret_cast<char*>(&frodo), sizeof(Person));
          fin.close();
      }
    

Resources