第二章 基本类型
1.const & 引用
const int a1=1024;
int &b1 =a1;//错误,不能用非常量引用指向常量
int &b1 =0;//错误,与上面类似
int a2=1024;
const int &b2=a2;//正确,但是可以用常量引用指向非常量(在初始化时)
//同时,以下非常量初始化都是正确的
const int &b2=1;
const int &b2=a2*4;
//从变量名p开始,从右往左读,更容易理解
const int &p;//const 保护的是&p,即p指向的值不能变
int &const p;//const 保护的是p,即p本身的引用目标不能变
在初始化时,这些右值都被编译器隐式的转换成了const xxx类型,所以可以在引用初始化时这样操作。如果让一个非const类型的引用b1指向const类型的值a1,程序员必然是想通过b1改变a1的值的,而改变a1的值是非法的,所以编译器不允许这样。
从右往左读:
变量p之前第一个符号是&,代表这是个引用;再之后是int,代表这是个指向int类型的引用;最后是const,代表引用指向的数受保护。
而如果第一个是const,代表p本身是受保护的,即p存储的引用目标不能变。但是引用目标本身如果值是可变的,p的值也就变了。
指针同样的方式理解。
2.const * 指针
const int *p;
/*正确,因为const限制的只是*p的值,而非p本身存储的地址。
既然p存储的地址可以改变,那么就没必要在初始化时要求必须赋值。
因为即使是int *p本身就是合法的,一个野指针。
但是p只能指向一个const int 类型的值,不能是int*/
int *const p;
/*错误,常量指针必须被初始化,可以指向一个非常量的普通int
类型的变量 */
const int a1;
int *b1 =&a1;//错误,与引用类似,普通指针不能指向常量
顶层指针:int *const p1;
底层指针:const int *p2;
顶层指针在赋值时不受影响,底层指针赋值时,双方必须都有底层指针才能赋值。
int i=0;
int *const p1=&i;//顶层
const int ci;//顶层,变量不变都是顶层
const int *p2=&i;//底层
const int *const p3=&i;//顶层、底层都有
int *p=p3;//错误,p没有底层
p2=p3;//正确,都有底层
- 一些关键词
constexpr int a=20;//常量表达式,编译时就能得到结果
//类型指示符,()内的类型为需要定义的类型
decltype(f()) a=b;
decltype((a)) c=a;//双括号时代表该类型的引用
:: global_a;//::代表 作用域为全局作用域
第三章 字符串、数组
- string
getline(cin,line);//读取一整行到line中
for(auto i:line);//遍历line中每一个元素
for(auto &i:line);//将i定义为引用,可以改变元素
- vector
vector是模板而非类型,所以初始化要使用vector来告诉类模板元素具体是什么类型。
对vector使用迭代器和范围for循环时,都不允许向该vector添加元素。
vector<int> a;
for(auto &i:a);
auto i=a.begin();//这两种情况下vector都不能添加元素
- 数组
定义数组时,[ ]内的数必须是const类型。
//数组的指针同样从左往右读,有括号先读括号
int *p[10];//包含10个元素的数组,*元素是指针,10个指向int型的指针
int (*p)[10];//p是指针,指向int型,指向大小为10的int数组
int a[10];
int *l=begin(a);//begin(),end()函数返回了
int *r=end(a);//指向数组起始位置和尾后位置的指针
//数组初始化
//三种方式都可以,内部的小{}不是必须的
int a[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};
int b[3][4]={{0,1,2,3},{4,5,6,7},{8,9,10,11}};
int c[3][4]={{0,1},{4,5,6},{8}};
第四章 表达式
- 表达式
//==运算符
if( (i=get_value()) != 10));//更好的写法,更易理解
int a;double b;
a=b=5.6;//右边优先算,b=5.6,a=5
b=a=5.6;//a=5,b=5.0
//简洁的写法
while(*p)
cout<<*p++<<endl;//++运算符优先级高于*
//? : 条件运算符可以嵌套
f=(a>90)? "high"
: ((a>60) ? "pass" : "fail");
//sizeof 运算符
int a[10];int *p=a;
n1=sizeof (a)/sizeof (*a);// 40/4=10
n2=sizeof (p)/sizeof (*p);// 8/4=2 或者 4/4
//指针在32位系统是4个字节,64位系统是8个字节
//而int都是4个字节
//,运算符
f=a++,b--,c++;//从左到右依次运行,最后只将最右侧c++的值赋给f
//显示转换
static_cast<int>(a);//最常用的强制类型转换,编译器不会发出警告
int b;
void* p=&b;//void*可以存储指向任意类型的指针,但是p不能被使用
int *f=static_cast<int*>(b);//可以通过这样使p保存的值有效,前提是类型一致
const int *p;
const_cast<int*>(p);//可以去掉p的底层const属性,具体用法目前不了解,书上说函数重载时会用到
第六章 函数
1.形参
当不改变形参的值时,尽量使用const T &来定义形参:
bool is(const int &a, const int &b);
形参在传递数组时,实际传递的是数组首元素的指针,并不能限制数组的大小。
//这三种写法的作用是完全一样的,不能限制数组的大小
void p(int *);
void p(int []);
void p(int [10]);
但是可以通过传递固定大小数组的指针或者引用来限制。
void p(int (&a)[10]);//指向大小为10的int数组的引用
void p(int (*a)[10]);//指向大小为10的int数组的指针
int i[10];
int (&a)[10]=i;//a就是数组i的别名,a[n]=i[n]
int (*b)[10]=&i;//b就是指向数组i的指针,(*b)[n]=i[n]
p(a);p(b);
可变参数initializer_list
initializer_list lst;
类似于vector,只不过它存储的数据初始化之后就不能改变或者增加减少,即为常量值。在类的构造函数中可以作为形参传递进来,相对来说使用时比较灵活:
class num{
num(initializer_list<int> p)
{
}
};
initializer_list<int> p={1,2,3,4};
num a({1,2,3,4});
num b(p);
2.return返回值
返回数组的指针
由于指向数组的指针写起来比较复杂,最简单的办法就是先使用类型别名定义这种类型.
typedef int arrT[10];//arrT是一个类型别名,表示含有10个int的数组
using arrT=int[10];//与上面的等价
arrT* func(int i);//返回一个指向10个int的数组的指针
int (*func( int i))[10];//如果不使用别名,需要这样写
尾置返回类型,在返回类型比较复杂时,可以使用这种方式来使返回类型看起来比较清晰
auto func(int i) -> int(*)[10];
3.函数重载
需要清晰的表达形参的不同才能使用函数重载
//这种无法通过编译
const int print(const int a,const int b);
int print(int a,int b);
4.函数声明
函数声明可以出现的任何地方,所出现的地方就决定着该声明的作用域。
void print (int a, int b);
void main()
{
//这么做是合法的,在这里声明一个带默认实参的函数
void print (int a=1, int b=2);
print();
}
int c1=1,c2=2;
void print (int a=c1, int b=c2);
void main()
{
//改变默认实参所被赋予的值,也能改变默认实参
c1=3;c2=4;
print();//(3,4)
}
5.指向函数的指针
bool lengthcomp(const string &, const string &);
bool (*pf)(const string &, const string &)=lengthcomp;
//pf是一个指向
// 形参为(const string &, const string &),返回值为bool的函数
pf=lc;
pf=&lc;//这两种效果是等价的,会自动将函数名转换为取函数地址
指向函数的指针作为另一个函数的形参出现时,不需要强调它是函数指针的写法,函数被调用时也会自动转换为指向函数的指针
void comp(const string &, bool (*pf)(const string &, const string &));
void comp(const string &, bool pf(const string &, const string &));
//这两种定义是等价的,在我理解是因为形参中不可能出现函数声明,所以不会有歧义
comp(s1,lengthcomp);//会自动将lengthcomp转换为指向lengthcomp的指针
//为了简化声明,一般会用别名来定义
typedef bool (*pf)(const string &, const string &);
using pf=bool (*)(const string &, const string &);
typedef bool f(const string &, const string &);
using f=bool (const string &, const string &);
void comp(const string &,pf);//简化的函数声明
void comp(const string &,f);
6.返回指向函数的指针
using pf =int(*)(int*,int);
using f =int (int*,int);
pf fun(int);
f* fun(int);
auto fun(int) -> int(*)(int*,int);//这三个定义是等价的
第七章 类
1.this指针
类的成员函数中会隐式的传入this指针,可以利用this指针显式的指向类内成员变量。并且可以通过返回*this,实现返回对象的引用
class data{};
data & data::combine (const data &tmp)
{
data.a+=tmp.a;
return *this;
}
还可以在类内成员函数中传递*this实现外部函数可以操作类的对象的资源.
data::data ()
{
read(*this);
}
void read(data tmp)
{
tmp.a=1;
}
如果在类的成员函数上返回*this指针,可以多次调用对象。
class data
{
data &set(){return *this};
data &move(){return *this};
}
data tmp;
tmp.set().move();
2.=default
在构造函数后加上=default,要求编译器生成默认的构造函数。
data()=default;
3.友元函数
可以在类内加入友元声明来允许外部的函数、类、类的函数来访问自己的私有成员。
void read(data tmp);//友元声明无法替代正式声明,在调用之前需要声明该外部函数
class data
{
friend void read(data tmp);
friend class data2;
friend void data2::set();
}
4.类的其他特性
类中可以定义别名,该别名的作用域只限制在类内。
在类的声明中写函数定义(即写在类内部中的),是默认作为inline内联函数的。写在外部的需要手动添加。
mutable可变数据成员,某些函数被定义成const时,不允许修改类内变量,而mutable变量可以绕过这种限制。
class data
{
void some() const
{
a++;
}
mutable int a;
};
类的前向声明,在类被正式声明之前,可以做一个简单的声明来使其他函数能够识别,但是前向声明不能代替正式声明。
class data;
5.构造函数,初始化
类的成员初始化顺序与其被定义的顺序相关,与初始化列表顺序无关
class data
{
int a;
int b;
int c;
data(int i, int j):b(i),a(j){}//安装a,b,c的顺序初始化
}
默认实参的构造函数
这种构造函数相当于提供了默认构造函数,因为不提供实参也能创造类的对象。
class data
{
data(int tmp=0):a(tmp);
}
data data1;
委托构造函数,我们可以在构造函数的初始化列表中调用其他构造函数来完成构造函数的工作
void read(string tmp,data data1)
{
data1.i=sizeof(tmp);
}
class data
{
data(int i, int j, int k):b(i),a(j),c(k){}
data():data(0,0,0);//委托构造函数
data(int i):data(i,0,0);
data(string tmp):data()
{
read(tmp,*this);
}
}
explicit,禁止构造函数进行隐式转换
当构造函数带参数时,可能会进行隐式的参数转换,可以利用explicit关键字来要求参数必须是要求的类型。
由于当构造函数带多参数时,不允许进行隐式的转换,所以explicit关键字只对一个参数的构造函数有效,并且只能用在类的声明处。
class data
{
explicit data(int i):b(i){}
6.聚合类
符合特定要求的类叫做聚合类:所有成员都是public、没有定义构造函数、没有初始值、没有基类虚函数。
聚合类可以在实例时通过花括号直接初始化。
struct data
{
int a;
string s;
};
data data1={1,"abc"};
7.static,静态成员
类的静态成员储存于类的任何对象之外,即实例化的每一个对象的内存中并不包含静态成员。并且所有的对象共有类的静态成员,而每个静态成员都只有一个。所以静态成员也不包含*this指针。
类的静态成员可以在类内定义也可以在类外定义,而static关键字只能出现在类的声明处。类外定义与普通成员函数相同,不同的是类外定义静态成员变量时可以调用private函数。
class data
{
privata:
static int rate ;
int initrate();
};
void data::rate=initrate();
静态成员一般不在类内初始化,因为类内初始化需要创建对象时才会调用。静态成员的类型可以是类本身,而普通成员不可以。
class data
{
privata:
static data data1;//正确
data data2;//错误
};
第九章 容器
1.iterator迭代器
迭代器无法比较大小,只能比较是否相等:it1!=it2
2.标准库array
array<int,10> a;//初始化必须定义类型和大小
array<int,10> a1={0,1,2};//可以列表初始化
a=a1;//可以直接拷贝
2.assign,重新定义数组
assign会完全替换原来的数组
vector<int> a(1),b(10);
a.assign(b.begin(),b.end());
a.assign(10,1);
3.insert()的返回值
insert函数的返回值为插入的新元素中的第一个新元素,而insert函数作用位置是在目标位置之前插入新元素。
4.emplace,调用构造函数
emplace可以直接将容器的对象构造出来,如果这个对象有构造函数的话。
class data
{
data();
data(int a, int b);
}
vector<data> d1;
d1.emplace_back();
d1.emplace_front(1,2);
d1.emplace(iteratoe,1,2);//在迭代器位置之前插入
5.capacity,不重新分配内存时容器可以存储多少元素
6.string操作
string初始化时可以从char[]数组或者其他string中拷贝
char c[]="hello world";
string s1(c);
string s2(c,3);//从c中拷贝3个字符
string s3(c+5,6);//从c[5]开始拷贝6个字符
string s4(s1,6)//从s1[6]拷贝到结尾
string s5(s1,6,3)//从s1[6]开始拷贝3个字符
substr可以返回string的子序列
string s1=s.substr(0,5);//s[0]到s[5]
string s2=s.substr(6);//s[6]到结尾
string可以接受数字作为插入的位置
char cp[]="hello world";
s.insert(s.size(),cp+7)//在s的结尾插入从cp[7]开始到结尾的字符
string s1="hello world";
s.insert(0,s1);//在0位置插入s1
s.insert(0,s1,0,5);//在0的位置插入s1中s1[0]到s1[5]的字符
append操作是在string的结尾出增加字符的功能
replace则是在指定位置替换字符
s1.append("123");//末尾出增加123字符
s1.replace(10,3,"12345");//在s1[10]开始的3个字符替换
find搜索操作
详见325面
字符类型转换
string s= to_string(i);//将数值转换为string类型
double d=stod(s);//将string转换为double
stoi();stof();//等等
第十章 泛型算法
1.一些标准算法库的函数
int sum = accumulate(vec.cbegin(),vec.cend(),0);//求和算法
int sum = accumulate(str.cbegin(),str.cend(),string(" "));//将所有的字符拼接并加上空格
fill(vec.begin(),vec.end(),0);//在一个范围内替换成0
copy(vec1.begin(),vec1.end(),vec2.begin());
//把范围内的数据拷贝到最后一个迭代器位置,要求必须有足够的空间
replace(vec.begin(),vec.end(),0,42);//在范围内搜索到的0替换为42
replace_copy(vec.begin(),vec.end(),back_inserter(vec2),0,42);
//在范围内搜索到的0替换为42,把替换后的数据拷贝到迭代器位置,vec原数据不变
auto end_unique = unique(vec.begin(),vec.end());//消除重复元素
//{1,1,2,2,2,3,3,3,4} -- > {1,2,3,?,?,?,?,?,?},只保证不重复元素在前面,返回最后一个不重复元素(3)之后的位置
stable_sort(words.begin(),words.end(),isShorter);//相同长度的单词会维持原有顺序,sort函数会使其随机排列
find_if(words.begin(),words.end(),func);
//如果满足定制函数要求的数,则返回该元素的迭代器,否则返回end迭代器
for_each(words.begin(),words.end(),func);
//遍历函数,func可以使用lambda表达式,比for循环效率高且简洁
transform(v1.begin(),v1.end(),v1.begin(),func);
//转换函数,便利前两个参数构成的输入序列,把输入序列的元素转换成目标序列后,放在第三个参数指向的起始位置
2.插入迭代器
vector vec;
auto it1=back_inserter(vec);//指向到容器尾部
auto it2=front_inserter(vec);//指向到容器头部
auto it3=front_inserter(vec,it1);//指向到容器it1迭代器,插入到该值之前
*it1=3;//在指定位置插入3
3.谓词函数
标准库算法可以使用谓词定制,包括一元谓词和二元谓词
bool isShorter( const string &s1,const string &s2)
return s1.size()<s2.size();
sort(words.begin(),words.end(),isShorter);
4.lambda表达式
[捕获列表](形参) -> 返回类型 { 函数体 }
捕获列表为要传入该表达式内计算的实际变量(只能是局部变量,其他诸如全局变量可以直接调用),形参和返回类型都可以省略不写。
auto f=[]{ return 42;}//形参和返回类型都省略,根据return判断返回类型
sort(words.begin(),words.end(),
[]( const string &s1,const string &s2) {return s1.size()<s2.size();});//使用lambda表达式实现
int sz=42;
find_if(words.begin(),words.end(),[sz](const string &s1){return s1.size()>sz; });//找到长度对于sz的第一个数据
auto f=[sz](const string &s1){return s1.size()>sz;
find_if(words.begin(),words.end(),f);//也可以这样调用
auto f=[sz](const string &s1){return sz };
auto f=[&sz](const string &s1){return sz };//引用捕获,捕获的sz会随之sz值的变化而变化
[&]//对所有局部变量都是引用捕获
[=]//对所有局部变量都是值捕获
[&,a,b,c...]//除了abc是值捕获,其他都是引用捕获
[=,&a,&b,&c...]//除了abc是引用捕获,其他都是值捕获
5.bind函数,参数绑定
需要频繁调用的时候,用lambda表达式并不方便,可以用参数绑定的形式
auto NewCallable = bind(Callable,list)
即将list作为Callable的形参填充进去,其中list需要包含一元或二元谓词,用_1,_2来表示
bool check_size(const int &s, int sz)
return s>sz;
using namespace std::placeholders;//调用_1,_2需要此命名空间
auto wc = find_if(words.begin(),words.end(),
bind(check_size,_1,sz));//将_1,sz作为形参调用check_size
bool func(int x1,int x2,int x3,int x4,int x5,int x6);
bind(func,a,b,c,_1,d,_2);
//相当于调用了:
func(a,b,c,_1,d,_2);//其中_1,_2是标准算法函数需要的二元谓词,可通过改变顺序达到相反的效果
第十一章 关联容器
1.map,set
map<key,value> 建立一一映射的关系
set< value > 只保存值
//初始化
map<string,string>={{"C++","Primer"},{"Hello","world"}};
set<string>={"C++","Primer","Hello","world"};
//不管是初始化,还是增加元素,map和set都只保存非重复元素,对于重复元素会自动被剔除
//但是multimap和multiset会保存重复的元素,并且重复的元素相邻存储。可以使用迭代器确定重复元素的始末位置
multimap<string,string>={{"C++","Primer"},{"C++","Primer"}};
multiset<string>={"C++","C++","C++","C++"};
2.pair类型
pair类型就是map中存储的类型,key和value关联之后的类型就是pair类型
pair<T1,T2> p;//p的两个内部元素被默认初始化
pair<T1,T2> p(v1,v2);
pair<T1,T2> p={v1,v2};//用v1,v2给两个元素初始化
auto p=make_pair(v1,v2);返回一个pair类型
p.first;p.second;//两个成员元素
3.关联容器操作
map<string,int>::key_type v1;//第一个元素的类型,string
map<string,int>::mapped_type v2;//第二个元素的类型,int
map<string,int>::value_type v2;//pair类型,pair<string,int>
//注意:返回的pair类型不能改变第一个元素的值,但是可以改变第二个元素
//insert操作,无需指定插入位置,因为map本身是自动有序的
map<string,int> p;
p.insert({"C++",1});
p.insert(make_pair("C++",1));
p.insert(pair<string,int> ("C++",1));
p.insert(map<string,int>::value_type ("C++",1));
//insert返回一个pari类型来告诉调用者插入是否成功(因为map不会插入重复元素),pair的第二个值是bool类型表示成功失败。
//pair的第一个值是指向XX的迭代器:指向成功插入后的元素对pair 或者 已存在的元素对pair
auto ret=p.insert({"C++",1});
if(ret.second)
cout<<ret.first->second;
else
++ret.first->second;//ret是pair类型的,而ret.first是指向pair的迭代器,所以需要用->
//erase操作,返回删除元素的数量
auto cnt=p.erase("C++",1);//返回删除0,1个,如果是multimap可能是更多
//下标操作
p["C++"]=1;//如果存在c++,把对应的值改成1;如果不存在,直接增加一对
++p["C++"];//返回的是一个左值,也可以这样操作
p.at("C++");//这个只能索引存在的元素,如果不存在会抛出异常
p.find("C++");//返回指向第一个该元素的迭代器或end迭代器
p.count("C++");//返回该元素的数量
p.lower_bound("C++");//返回第一个不小于其的元素
p.upper_bound("C++");//返回第一个大于其的元素
p.equal_range("C++");//返回一个迭代器pair,表示与其相等的范围
multimap<string,int> p={...};//一个打印所有相同关键字的值的例子
for(auto range=p.equal_range("C++");range.first != range.second; ++range.first)
cout<<range.first->second<<endl;
第十二章 动态内存和智能指针
1.shared_ptr
//make_shared函数可以从动态内存中分配一个智能指针对象并初始化
shared_ptr<int> p=make_shared<int>(42);//()内可以用来调用构造函数初始化
shared_ptr<string> p=make_shared<string>(10,'9');//"999999999"
auto p=make_shared<string>(10,'9');//简单的写法
p1=p2;//shared_ptr有计数器,p1原先指向内存计数器减1,p2指向内存计数器加1
//当计数器为0时,指向的对象会被自动释放
shared_ptr<int> p(new int(1024));//也可以通过直接申请内存的方式
return shared_ptr<int>(new int(1024));//需要返回智能指针时,也可以这样显示的生成
if(!p.unique())//判断p是否是该内存唯一的拥有着
p.reset(new int(1024));//reset时引用会减1,为0时会自动释放;然后重新指向新分配的内存
data d;
void end_connection(data* p) {...};
shared_ptr<data> p(&d,end_connection);//当p的生命周期结束时,调用end_connection而非delete来进行释放
2.unique_ptr
//unique_ptr是内存的唯一拥有着,不支持拷贝和赋值
unique_ptr<int> p(new int(1024));
p.release();//p放弃对指针的控制权,并将p置为空,但并未释放对象,返回该对象
p.reset();//释放对象,并置为空
p.reset(q);//释放对象后,指向q
p.reset(q.release());//q先放弃、置空并返回原对象,p释放原对象转而指向了q的原对象
unique_ptr<int> f(int p)
{
return unique_ptr<int>(new int(p));
//虽然不能拷贝unique_ptr,但是当它即将被销毁时,可以返回它(即使这个过程会有拷贝)
}
3.weak_ptr
//weak_ptr不引入计数器,对对象只有观测权没有管理权
weak_ptr<T> w(sp);//sp是shared_ptr
w.reset();//置为空
w.use_count();//观测对象的shared_ptr数量
w.expired();//不存在shared_ptr用户时返回true
w.luck();//判断指向的对象是否存在,如果存在返回一个指向该对象的shared_ptr;否则返回一个空的shared_ptr
if(shared_ptr<int> p=w.luck())//判断w指向的对象是否存在
{}
第十三章 拷贝控制
1.拷贝构造函数/拷贝赋值运算符/析构函数
在不定义拷贝构造函数的时候,编译器会为我们自动生成一个,它会将类内所有的变量值从目标类中直接赋值过来。同样的,编译器也会为我们自动生成一个拷贝赋值运算符,以完成=的操作。
如果一个类需要定义析构函数,那么它一定需要定义自己的拷贝构造函数和拷贝赋值运算符。
class data
{
public:
~data() {delete p;}
private:
int* p;
};
data a;
data b(a);
//如果不定义拷贝构造函数,b.p和a.p为同一个指针,当a被销毁而b还没有被销毁时
//b.p却已经被销毁了,会带来内存错误
class data
{
public:
~data() {delete p;}
data(const data& d):p(new int(*d.p))//需要为这种情况申请新的内存
void operator=(const data& d) {p= new int(*d.p)}
private:
int* p;
};
class data
{
public:
data()=default;//显式的告诉编译器使用自动生成的默认构造函数
data()=delete;//显式的告诉编译器不要自动生成
2.拷贝控制和内存资源管理
class data
{
data& operator=(const data& d)//编写拷贝赋值运算符时,需要考虑=自身的情况
{
auto newp=new int (d.p);//需要创造一个临时变量保存目标值
delete p;//以防止当=右边的值是自己时,被误删
p=newp;
return *this;
}
data& operator=(std::initializer_list<std::string> il)
//特别的,可以将形参写为初始化列表来实现={“a”,“b”};的操作
{
a=il[0];
b=il[1];
}
public:
void swap(data &l,data &r)//定义自己的swap操作,可以提高效率
{
using std::swap;//调用std的swap
swap(l.p,r.p);//直接交换指针,而不是数据,可以提高效率
}
data& operator=(data d)//swap版本的赋值运算符,形参不是引用而是按值传递的
{
swap(*this,d);//交换之后,d在函数结束后会被销毁
return *this;//但是这种做法需要重新构造一个data类对象,效率不见得更高
}
}
3.移动语义
这一部分先看了这个链接,讲了把基本的东西讲的更多一些:右值引用、移动语义。
int &&a=1;//右值引用,只能绑定到右值上
int &&b=a*10;
int &&c=a;//错误,不能绑定到左值上,即使a是一个右值引用也不行
std::move(tmp)
//使用move操作之后,不应该再调用tmp使用其原始值,原始值可能已被销毁,但是可以给它赋新值
//移动语义可以提高效率,前提是要定义移动构造函数和移动赋值函数
class data
{
private:
char* m_data;
public:
data(const char* cstr=0)// 构造函数
{
if (cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}
else {
m_data = new char[1];
*m_data = '\0';
}
}
data(const data& str)//拷贝构造函数
{
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);//都是通过拷贝内存完成的
}
data(data&& str) noexcept//移动构造函数,noexcept表示告知编译器不会抛出异常
:m_data(str.m_data)//直接通过将指针指向目标内存,可以避免拷贝
{
str.m_data = nullptr; //把原右值置空
}
data& operator=(const data& str)// 拷贝赋值函数 =号重载
{
if (this == &str) // 避免自我赋值
return *this;
delete[] m_data;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
return *this;
}
data& operator=(data&& str) noexcept// 移动赋值函数 =号重载
{
if (this == &str) // 避免自我赋值
return *this;
delete[] m_data;
m_data = str.m_data;//同样是通过改变指针的方式
str.m_data = nullptr; //把原右值置空
return *this;
}
}
data tmp("hello");//构造对象时,调用拷贝构造函数,需要反复赋值
data tmp2(std::move(tmp)); //调用的是移动构造函数,效率更高
//当tmp值不再需要时,使用move来提高效率,比如:
vector<data> data1;
data1.push_back(std::move(tmp));
//push_back的函数调用时会构造形参,这时就会触发:data tmp2(std::move(tmp))
data1.empalce_back(std::move(tmp));
//对于stl库,调用empalce_back会避免一次无谓的拷贝,因此应该使用empalce_back代替push_back
移动操作的本质就是将需要移动的对象转换成右值,然后通过更改被赋值对象的指针,将其直接指向右值来接管,同时源移动对象指向它自身的指针需要置空。
基本数据类型和所有的STL类型都支持移动操作,如果类没有定义自己的拷贝控制函数,编译器会为这个类默认合成一个移动构造函数。(比如没有指针,也没有static,都是数据对象的类)
class data
{
//通常会定义两个版本的函数来支持两种操作
void push_back(const string &);//拷贝版本
void push_back(string &&);//右值,移动版本
}
class data
{
//如果我们希望限定只能被右值会左值调用,可以使用引用限定符
void sort() &&;//只能被右值调用
void sort() const &;//只能被左值调用
}
data val();//返回右值
data &foo();//返回左值
val().sort();//右值版本
foo().sort();//左值版本
//对于名字相同,参数列表相同的重载函数,要不然都加引用限定符,要不然都不加
第14章 重载运算与类型转换
1.重载运算基本概念
运算符函数可以是类的成员函数,也可以是类外普通函数。区别在于:
- 如果是类成员函数,则它会隐式的绑定一个this指针作为该运算符左侧的对象,而其形参列表中只需要填写右侧的对象。
- 如果是普通函数,则需要在形参列表中依次填写左右侧的对象
对于需要与其他类型交互的运算符,需要写成类外普通函数。
string s1="world";
string s2="hi" + s1;//这种情况如果+重载为类内成员函数,会产生错误
2.输入输出运算符(类外普通函数)
<<和>>输入输出运算符必须是类外函数,因为我们无法在标准库的标准输出流iostream中添加成员函数。
/*输出运算符*/
ostream &operator<<(ostream &os,const data &item)//顺序必须是ostream在前
{
os<<item.a<<item.b;
return os;
}
cout<<data;//我们只能将其写成类外普通函数,因为无法改变标准库
friend ostream &operator<<(ostream &os,const data &item);
//operator<<和operator>>通常应该被声明为友元,因为它们通常要访问私有成员
/*输入运算符*/
istream &operator>>(istream &is,const data &item)//输入运算符需要考虑输入失败的情况
{
double price;
is>>item.bookNO>>item.unit_sold>>price;
if(is)//is是cin的引用,cin内有输入失败则返回false的设计
item.revenue=item.unit_sold*price;
else
item=data();
return is;
//这也是为什么我们可以用while(cin>>a){};来实现持续输入的原因
}
3.算术和关系运算符(类外普通函数)
+,-,×,/,==,>,<这些运算符一般不会改变运算对象的状态,所以形参都是常量引用。
data operator+(const data &l,const data &r)
{
data sum=l;
sum.p=l.p+r.p;
return sum;
}
4.下标运算符(类内成员)
表示容器的类会定义下标运算符[ ],一般会定义两个版本,返回普通引用和返回常量引用。
class data
{
public://调用哪一个版本取决于调用的data对象是常量还是非常量的
std::string &operator[](std::size_t n)
{
return p[n];
}
const std::string &operator[](std::size_t n) const
{
return p[n];
}
private:
std::string *p;
}
data a;
const data b=a;
a[0]="a";//正确
b[0]="b";//错误,这个调用的是常量版本
5.递增递减运算符(类内成员)
class data
{
private:
int t;
public:
data& operator++()//前置运算符
{
++t;
return *this;
}
data& operator++(int)//后置运算符,通过一个不被使用的int来区分
{
data tmp=*this;
++*this;//调用前置运算符
return tmp;
}
}
data tmp;
tmp.operator++();//可以显式的调用前置运算符
tmp.operator++(0);//后置运算符
6.函数调用运算符(类内)
通过重载函数调用运算符,可以较为灵活的使用类的对象,比如:
struct absInt
{
int operator()(int val) const
{
return val<0 ? -val:val;//返回val的绝对值
}
}
更重要的,可以把类的对象传入标准算法库中的函数中,而这个对象其实就是lambda表达式的实质
stable_sort(words.begin(),words.end(),
[](const string &a,const string &b) {return a.size()<b.size(); })
//这句调用lambda表达式的本质其实就是创建一个类的未命名对象
class ShorterString
{
bool operator()(const string &a,const string &b) const
{
return a.size()<b.size();
}
}
stable_sort(words.begin(),words.end(),ShorterString());
//ShorterString()创建了一个默认构造函数的对象,并将对象传了进去,这个就是lambda表达式的本质
find_if(words.begin(),words.end(),
[sz](const string &a) {return a.size()<sz; })
class SizeComp
{
SizeComp(int n):sz(n){}//通过构造函数实现lambda中的捕获变量
bool operator()(const string &a) const
{
return a.size()<sz;
}
private:
int sz;
}
find_if(words.begin(),words.end(),SizeComp(sz));
标准库中提供了一组表达算术运算符的类,以便于我们在调用标准算法函数时方便改变条件,比如
#include <functional>
sort(words.begin(),words.end(),greaterr<string>);//变成降序排序
更一般的,我们可能需要建立一个函数表,来实现调用各种指向函数的指针
map<string,int(*)(int,int)> binops;//形参是(int,int),返回值是int的指向函数的指针
//如果我们这样定义,第二个元素必须是一个指向函数的指针,而不能是lambda表达式
//尽管他们都能取得相同的效果,所以标准库提供了function模板来解决这个问题
//只限定形参和返回值,而不限定实现的方式
function<int(int,int)> f1=add;//函数指针
function<int(int,int)> f2=GreaterInt();//函数对象类的对象
function<int(int,int)> f1=[](int i,int j){return i*j};//lambda表达式
map<string,function<int(int,int)>> binops;//因此我们可以重新定义map
binops.insert("+",add);
binops["+"](1,2);
binops["-"](1,2);
binops["*"](1,2);
//特别的,如果add被重载了,则不能直接插入map中
int (*fp)(int,int) =add;
binops.insert("+",fp);//直接传指向该add版本的指针
binops.insert("+",[](int i,int j){return add(a,b);};//或者直接写lambda表达式
7.类型转换运算符
类型转换函数实现类向其他类型互相转换的功能,隐式转换就是由这个实现的
operator type() const;//类型转换函数不能声明返回类型,形参列表必须为空,通常也应加上const
class data
{
public:
data(int i=0):val(i){}//构造函数就可以实现其他类型向data转换
operator int() const//把自身转换成int类型
{
return val;
}
}
data s;
s=4;//先把4隐式转换为data,然后再相加
s+3;//先把s隐式转换为int,然后再相加
//但是这些隐式转换可能会带来很多麻烦,因此可以要求必须显式要求类型转换时才调用
class data
{
public:
data(int i=0):val(i){}
explicit operator int() const//不会隐式的转换
{
return val;
}
}
data s;
stable_cast<int>(si)+3;//只有显式的要求时才会调用转换函数
//特别的,被用作条件时(比如if),会自动的向bool类型隐式转换
while(cin>>val);
- 如果我们定义了A类到B类的类型转换,则不要再定义B类到A类
- 避免定义向内置算术类型的类型转换
- 不要同时定义类型转换函数和对应的重载运算符,编译会遇到二义性问题
不过书上最终建议应该尽量避免使用类型转换,因为容易出现问题。
第15章 面向对象程序设计
1.基类和派生类
//3种继承方式
public//父类的public和protected仍然是原类型,private不继承
protected//父类的public和protected都继承为protected,private不继承
private//父类的public和protected都继承为private,private不继承
class data
{
public:
data(int a):tmp(a){}
cal();
private:
int tmp;
}
class datason : public data
{
data(int a):data(a){}//可以直接调用父类的构造函数
}
class datason2 final//如果我们不希望该类被继承,可以使用final关键字
{}
class datason3 : public data
{
public:
using data::tmp;//可以使用using改变父类对象的访问级别
private:
using data::cal;//函数也可以
}
data data1;
datason datason1;
//基类和派生类的对象不可以互相转换,但是如果将派生类对象转为指针或引用
//则可以实现该指针或引用向基类的隐式转换
class data
{
data(const data &);
}
data data2(datason1);//将datason1的引用转换为了data,然后调用data(const data &);
2.虚函数
struct B
{
virtual void f1(int) const;
virtual void f2();
virtual void f3()=0;//纯虚函数,不存在函数体,但是需要实例化的子类必须定义函数体覆盖它
void f4();
void f4(int,int);
}
struct D1:B//对于struct,默认private继承;对于class,默认public继承
{
void f1(int) const override;//对父类虚函数的重载,override不强制要求,但最好有
void f2() final//final关键字,不允许D1的子类继续覆盖虚函数f2
{
B::f2();//如果要调用父类版本的虚函数,可以作用域符,否则会无限调用自己
}
}
D1 *dd=b;
dd->B::f1();//同样,如果要强行调用父类的虚函数,可以这样
struct D2:B
{
void f4(int);//如果基类f4不是虚函数,这样会直接隐藏(删除)基类的f4(),f4(int,int)
using B::f4;//为了避免这种情况,使用using可以直接使用所有重载版本的f4函数
}
3.拷贝、构造和析构函数
如果基类删除了自己的默认拷贝、构造函数,那么派生类的也会被自动删除。
class base;
class D: public base
{
public:
D(const D& D):base(d)//拷贝版本
D(D&& D):base(std::move(d))//移动版本
D operator=(const D & rhs)
{
base::operator=(rhs);//调用基类的拷贝赋值函数为基类数据赋值
...//然后再为派生类赋值
}
~D(){...};//与拷贝和构造不同的是,析构函数会在派生版本执行完之后
//自动执行基类的析构函数,因此析构函数只需要关注自己的成员即可
using base::basee;//如果使用using声明,会继承所有版本的构造函数,同时变为自己的构造函数,
//即相当于函数名base也变成了D,可以直接由D类调用自己的构造函数一样调用
}
第15章 模板与泛型编程
1.基本定义
//非类型模板参数
template<unsigned N,unsigned M>//N和M代表传入数组的大小
int compare(const char (&p1)[N],const char (&p2)[M])
{
return strcmp(p1,p2);
}
compare("hi","mom");//调用时,N=2 M=3
template<typename T>//类模板
class bob
{
bob get();//类内作用域不需要写bob<T>
};
template<typename T>//类模板的成员函数
int bob<T>::menber(){};
template<typename T>//如果类的成员函数也是一个模板函数
template<typename F>//需要把类模板写在前面,函数模板也在后面
int bob<T>::menber(T a, F b){};
template<typename T>
class bob
{
friend class tom1<T>;//跟bob由相同类型实例化的tom1对象是友元
template<typename X> friend class tom2;//任何tom2对象都是友元
friend class tom3;//tom3是普通类,都是友元
};
template<typename T>
class bob
{
static int a;//对于static对象,每个typename类型共享该类型下的static
}
template<typename T>
typename T::size_type get( );
//如果我们需要使用模板类型内的类型成员,需要显示的告诉编译器这是一个类型
extern template class<string>;//如果已在其他位置实例化过该类型,可以用extern声明来提高效率p598
2.模板实参推断
template<typename T>
int get(T a,T b);
double tmp;
get(tmp,tmp);//我们不需要显式的指定tmp类型,编译器可以推断
template<typename T1,typename T2,typename T3>
T1 get(T2 a,T3 b);
double tmp;int tmp2;
auto f<long>= get(tmp,tmp2);//编译器无法推断T1的类型,必须显式指定
T3 get(T1 a,T2 b);
auto f<int,double,long>= get(tmp,tmp2);//如果这样定义,则必须写全
template<typename T>
//如果需要使用形参类型,比如*迭代器,需要使用decltype在尾置类型中写出来
auto get(T a) -> decltype(*a)
{
return *a;
}
template<typename T>
f(T &&t);
f<int&>(tmp);//这时就会造成引用折叠
f(T &&t)
{
g(t);//void g(F &f);这时也会
}
T& &,T& &&,T&& &都会折叠成T&
T&& &&折叠成T&&
template<typename T>
void func(T &&arg)
{
finalFunc(std::foward<T>(arg));//这样写可以完美保留arg实参的左右值引用属性
}
//一个完美转发foward的例子
template<typename T>
void handleValue(T &value) { cout << "lvalue " << value << endl; }
template<typename T>
void handleValue(T &&value) { cout << "rvalue " << value << endl; }
template<typename T>
void processValues(T &&arg)
{
handleValue(std::forward<T>(arg));
}
template<typename T, typename ... Ts>
void processValues(T&& arg, Ts&& ... args)
{
handleValue(std::forward<T>(arg));
processValues(std::forward<Ts>(args) ...); // 先使用forward函数处理后,再解包,然后递归
}
int main()
{
int a=1;
string b="l";
processValues(1, b, std::move(b));
return 0;
}
4.可变参数模板
template<typename T,typename... args>//...表示还有未知数量的typename类型
void func(T t1,args... rest)//...表示还有未知数量的参数
{
sizeof...(args);//typename数量
sizeof...(rest);//函数形参rest数量
}
//一个求未知个参数的最小值函数的例子
template<typename T>
T minn(const T &a,const T &b)
{
return a<b ? a:b;
}
template<typename T,typename... args>
T minn(const T &a,const T &b,const args&... c)
{
return minn(minn(a,b),c...);
//递归调用自己,直到c...只包含一个参数时,调用T minn(const T &a,const T &b)
}
minn(1,2,3,4,5,6);
template<typename... args>
void func(bool mode,args... rest)
{
return print(mode,debug(rest)...);
//这样写与上面不一样的在于,如果rest包含 1,2,3...,n
//就相当于print(mode,debug(1),debug(2),...,debug(n));
}
5.模板特例化
//如果我们需要针对一种类型,特例化模板函数
template<typename T>
T func(T &&arg);
template<>
int func(int &&arg);//这样写,会针对这种类型特殊处理
template<>
class data<int>{};//类模板特例化
template<>//如果我们只需要针对某个类型(int),对类的某个成员函数做特例化
void data<int>::get(){};//int类型,调用该版本
template<typename T>
void data<T>::get(){};//除int类型外,get函数都调用正常版本