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
- C++11新的类型说明作符,让编译器推断变量类型,因此使用
- 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
的空间。可以通过capacity
和reserve
来干预内存分配
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新增标准库函数
begin
,end
,用来返回静态数组的头指针和尾指针
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
- 如果参数中有引用类型,将其声明为
- 数组做函数形参
- 遵循两个规则:
- 不允许拷贝数组
- 使用数组做参数时,传递数组名做指针指向数组首地址
void print(const int *) //三者等价
void print(const int[]) //只读数组,形参声明为const
void print(const int[10])
- 上述定义可以注意到:
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
- 如果函数的返回值类型不是void,那么函数必须有一个返回值,
- 数组不能拷贝,因此函数不能返回数组,但可以返回一个指向数组的指针
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];
它的含义如下:
func(int i)
表示调用func
函数时需要一个int
型的实参(*func(int i))
表示对函数调用结果进行解引用操作(*func(int i))[10]
表示解引用操作返回的是一个有10个元素的数组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));
上面声明太过复杂,可以使用typedef
和decltype
简化
//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);
从内向外读:
f1
有形参列表,说明f1是一个函数f1
左边有*
说明它的返回值是指针- 指针也有形参列表,说明返回的是一个函数指针
- 使用尾返回类型表示
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)返回的是函数类型,因此需要加上*,表明返回的是指针。
类
- 关键字
class
和struct
- 唯一区别是默认的访问权限
- 使用
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!,定义了一个函数
- 当类没有自定义构造函数时,编译器会默认生成一个构造函数(synthesized default constructor),
- 拷贝构造函数
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; }
- `const`成员函数不能修改成员变量,不能访问成员函数
类对象
- 常量对象
- 常量对象不能修改成员成员变量,不能访问成员函数
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)
聚合类满足以下几个条件:
- 所有成员都是public
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类,也没有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
对应标准错误输出流,用于向屏幕输出出错信息cerr
和clog
的区别在于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(); }
- 由于编译器有内存对齐的优化,因此