复习C++ Primer
constexpr变量
允许将变量声明为constexpr类型以便由编译器来验证变量的值是一个常量表达式,声明为cosntexpr的变量一定是一个常量,而且必须由常量表达式初始化:
1constexpr int mf = 20; //20是常量表达式
2constexpr int limit = mf + 1; //mf + 1是常量表达式
3constexpr int sz = size(); //只有当size是一个constexpr函数时才是一条正确的声明语句
4
5const int *p = nullptr; //p是一个指向整型常量的指针
6constexpr int *q = nullptr; //q是一个指向整数的常量指针
别名声明
新标准规定了一种新的方法,使用别名声明(alias declaration)来定义类型的别名:
1using SI = Sales_item; //SI是Sales_item的同义词
auto类型说明符
1auto item = val1 + val2; //item初始化为val1和val2相加的结果
2
3auto i = 0, *p = &i; //正确: i是整数, 这是整型指针
4auto sz = 0, pi = 3.14; //错误: sz和pi的类型不一致
5
6int i = 0, &r = i;
7auto a = r; //a是一个整数(r是i的别名,而i是一个整数)
auto一般会忽略掉顶层const,同时底层const则会保留下来,比如当初值是一个指向常量的指针时:
1const int ci = i, &cr = ci;
2auto b = ci; //b是一个整数(ci的顶层const特性被忽略掉了)
3auto c = cr; //c是一个整数(cr是ci的别名,ci本身是一个顶层const)
4auto d = &i; //d是一个整型指针(整数的地址就是指向整数的指针)
5auto e = &ci; //e是一个指向整数常量的指针(对常量对象取地址是一种底层const)
如果希望推断出的auto类型是一个顶层const,需要明确指出:
1const auto f = ci; //ci的推演类型是int, f是const int
其它还有一些示于代码:
1auto &g = ci; //g是一个整型常量引用,绑定到ci
2auto &h = 42; //错误:不能为非常量引用绑定字面值
3const auto &j = 42; //正确:可以为常量引用绑定字面值
4
5auto k = ci, &l = i; //k是整数,l是整型引用
6auto &m = ci, &p = &ci; //m是对整型常量的引用,p是指向整型常量的指针
7auto &n = i, *p2 = &ci; //错误:i的类型是int, 而&ci的类型是const int
decltype
- decltype((variable)) (注意是双括号)的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用。
理解复杂数组
1int *ptrs[10]; //ptrs是含有10个整型指针的数组
2int &refs[10] = /* ? */; //错误:不存在引用数组
3int (*Parray)[10] = &arr; //Parray指向一个含有10个整数的数组
4int (&arrRef)[10] = arr; //arrRef引用一个含有10个整数的数组
sizeof实现
1#define my_sizeof(L_Value) ( (char *)(&L_Value + 1) – (char *)&L_Value)
基于这一点:
int a[10];
&a+1为对于数组a整体+1
a+1为对于a中一个元素+1
命名的强制类型转换
1cast-name<type>(expression);
dynamic_cast
运行时类型识别
//todo
static_cast
任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。
1double slope = static_cast<double>(j) / i; //进行强制类型转换以例执行浮点数除法
使用static_cast找回存在于void *指针中的值:
1void *p = &d; //正确:任何非常量对象的地址都能存入void *
2double *dp = static_cast<double *>(p); //正确:将void *转换回初始的指针类型
const_cast
1const char *pc;
2char *p = const_cast<char *>(pc); //正确:但是通过p写值是未定义的行为
3
4const char *cp;
5char *q = static_cast<char *>(cp); //错误:static_cast不能转换掉const性质
6static_cast<string>(cp); //正确:字符串字面值转换成string类型
7const_cast<string>(co); //错误:const_cast只改变常量属性
reinterpret_cast
reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。
1int *ip;
2char *pc = reinterpret_cast<char *>(ip);
3
4string str(pc); //可能导致异常的运行时行为
使用reinterpret_cast是非常危险的!
范围for语句
//todo
默认形参
一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
=default的含义
C++11标准里面可以指定一个默认构造函数,但是我觉得没什么用
泛型算法
大多数算法都定义在头文件algorithm中,标准库还在头文件numeric中定义了一组数值泛型算法
智能指针
智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象
shared_ptr允许多个指针指向同一个对象; unique_ptr则“独占”所指向的对象,标准库还定义了一个名为weak_ptr的伴随类,这是一种弱引用,指向shared_ptr所管理的对象, 这三种类型定义在memory头文件中。
1shared_ptr<Foo> factory(T arg)
2{
3 //恰当的处理arg
4 //shared_ptr负责释放内存
5 return make_shared<Foo>(arg);
6}
1void use_factory(T arg)
2{
3 shared_ptr<Foo> p = factory(arg);
4 //当我们返回p时, 引用计数进行了递增操作
5 return p; //p离开了作用域,但它指向的内存不会被释放掉
=default 和 =delete
可以将赋值构造函数和赋值运算符, 析构函数定义为 =defalut 的
可以将赋值构造函数和赋值运算符,(析构函数不行)定义为delete的,这样编译器就不能默认生成它们。也可以将它们声明为private的,效果应该是一样的。
对象移动与右值引用
新标准的一个最主要的特征是可以移动而非拷贝对象的能力,为了支持移动操作,引入了一种新的引用类型 -- 右值引用
右值引用有一个重要的性质,只能绑定到一个将要销毁的对象,因此,我们可以自由的将一个右值引用的资源"移动"到另一个对象中.
1int i = 42;
2int &r = i; //正确: r 引用 i
3int &&rr = i; //错误: 不能将一个右值引用绑定到一个左值上
4int &r2 = i * 42; //错误: i * 42 是右值
5const int &r3 = i * 42; //正确: 我们可以将一个const的引用绑定到一个右值上
6int &&rr2 = i * 42; //正确: 将rr2绑定到乘法的结果上
由于右值引用只能绑定到临时对象,我们得知
- 所引用的对象将要被销毁
- 该对象没有其他用户
这两个特性意味着: 使用右值引用的代码可以自由地接管所引用的对象的资源.
标准库move函数
变量是左值,因此我们不能将一个右值引用直接绑定到一个变量上,即使这个变量是是右值引用类型也不行.虽然不能将一个右值引用直接绑定到一个左值上,但我们可以显式地将一个左值转换为对应的左值引用类型.
1int &&rr3 = std::move(rr1);
另,与大多数标准库名字的使用不同,对move我们不提供using声明,我们直接调用std::move而不是move.
移动构造函数和移动赋值运算符
定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作,否则,这些成员默认地被定义为删除的.
但是如果没有移动构造函数,右值也被拷贝
1class Foo {
2public:
3 Foo () = default;
4 Foo (const Foo &); //拷贝构造函数
5};
6
7Foo x;
8Foo y(x); //拷贝构造函数; x是一个左值
9Foo z(std::move(x)); //拷贝构造函数,因为未定义移动构造函数
下标运算符
如果一个类包含下标运算符,则它通常会定义两个版本: 一个返回普通引用, 另一个是类的常量成员并且返回常量引用
1class StrVec {
2public:
3 std::string & operator[] (std::size_t n){ return elements[n]; }
4 const std::string& operator[] (std::size_t n) const
5 { return elements[n]; }
6private:
7 std::string *elements;
8};
递增和递减运算符
区分前置和后置运算符
后置版本接受一个额外的(不被使用)int类型的形参.当我们使用后置运算符时,编译器为这个形参提供一个值为0的实参.尽管从语法上来说后置函数可能使用这个额外的形参,但是在实际过程中通常不会这么做.这个莆参的唯一作用就是区人前置版本和后置版本的函数,而不是真的要在实现后置版本时参与运算.
1class StrBlobPtr {
2public:
3 StrBlobPtr operator++(int); //后置运算符
4 StrBlobPtr operator--(int);
5};
对于后置版本来说,在递增对象之前需要首先记录对象的状态:
1StrBlobPtr StrBlobPtr::operator++(int)
2{
3 StrBlobPtr ret = *this; //记录当前的值
4 ++ *this; //向前移动一个元素,前置++需要检查递增的有效性
5 return ret; //返回之前记录的状态
6}
7
8StrBlobPtr StrBlobPtr::operator--(int)
9{
10 StrBlobPtr ret = *this; //记录当前的值
11 -- *this; //向后移动一个元素,前置--需要检查递减的有效性
12 return ret; //返回之前记录的状态
13}
显式的调用后置运算符
如果我们想通过函数调用的方式谳用后置版本,则必须为它的整型参数传递一个值:
1StrBlobPtr p(a1); //p指向a1中的vector
2p.operator++(0); //调用后置版本的operator++
3p.operator++(); //调用前置版本的operator++
运算符重载
可以被重载的运算符
+ - * / % ^
& | ~ ! , =
< > <= >= ++ --
<< >> == != && ||
+= -= /= %= ^= &=
!= *= <<= >>= [] ()
-> ->* new new[] delete delete[]
不能被重载的运算符
:: .* . ?:
函数调用运算符
函数调用运算符必须是成员函数.一个类可以定义多个不同版本的调用运算符,相互之间应该在参数上面有所区别
标准库函数对象
| 算术 | 关系 | 逻辑 |
|---|---|---|
| plus<Type> | equal_to<Type> | logical_and<Type> |
| minus<Type> | not_equal_to<Type> | logical_or<Type> |
| multiplies<Type> | greater<Type> | logical_not<Type> |
| divides<Type> | greater_equal<Type> | |
| modulus<Type> | less<Type> | |
| negate<Type> | less_equal<Type> |
在下面的例子中,我们定义了两种将B转换成A的方法:一种使用B的类型转换运算符/另一种使用A的以B为参数的构造函数:
1//最好不要在两个类之间构建相同的类型转换
2struct B;
3struct A{
4 A() = default;
5 A(const B&); //把一个B转换成A
6 //其他数据成员
7};
8
9struct B{
10 operator A() const; //也是把一个B转换成A
11};
12
13A f(const A&);
14B b;
15A a = f(b); //二义性错误:含义是f(B::operator A()) 还是 f(A::A(const B&)) ?
如果我们确实想执行上述的调用,就不得不显式地调用类型转换运算符或者转换构造函数:
1A a1 = f(b.operator A()); //正确,使用B的类型转换运算符
2A a2 = f(A(b)); //正确,使用A的构造函数
被用作基类的类
如果我们想将某个类用作基类,则该类必须已经定义,而非仅仅声明:
1class Quote; //声明但未定义
2class Bulk_quote : public Quote {...}; //错误:Quote必须被定义
定个规定是显而易见的, 派生类中包含并且可以使用它从基类而来的成员,为了使用这些成员,派生类当然要知道它们是什么.因此該规定还有一层隐含的意思,即一个类不能派生它本身.
防止继承的发生
在类名后添加一个关键字 final
1class NoDerived final { /* */ };
2class Base { /* */ };
3//Last 是 fina 的; 我们不能继承 Last
4class Last final : Base { /* */ }; //Last不能作为基类
5class Bad : NoDerived { /* */ }; //错误:NoDerived 是 final的
6class Bad2 : Last { /* */ }; //错误:Last 是 final 的
派生类向基类赋值
当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝,移动或赋值,它的派生类部分将被忽略掉.
默认的继承保护级别
人们常常有一种错觉,认为在使用struct关键字和class关键字定义的类之间还有更深层次的差别,事实上,唯一的差别就是默认成员访问说明符及默认派生访问说明符;除此之外,再无其他不同之处.
名字查找先于类型检查
声明在内层作用域的函数并不会重载声明在外层作用域的函数,因此,定义派生类中的函数也不会重载其基类中的成员.和其它作用域一样,如果派生类(即内层作用域)的成员与基类(却外层作用域)的某个成员同名,则派生类将在其作用域内隐藏该基类成员.即使派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏掉.
1struct Base {
2 int memfcn();
3};
4
5struct Derived : Base{
6 int memfcn(int);
7};
8
9Derived d; Base b;
10b.memfcn(); //调用Base::memfcn
11d.memfcn(10); //调用Derived::memfcn
12d.memfcn(); //错误:参数列表为空 memfcn被隐藏了
13d.Base::memfcn(); //正确:调用Base::memfcn
** 基类与派生的类的虚函数必须有相同的形参列表 ** 不然我们就不能通过基类的引用或指针调用派生类的虚函数了.
虚函数与作用域
1class Base {
2public:
3 virtual int fcn();
4};
5
6class D1 : public Base {
7public:
8 int fcn(int); //隐藏了基类的fcn,这个fcn不是虚函数
9 virtual void f2(); //是一个新的虚函数,在Base中不存在
10};
11
12class D2 : public D1{
13public:
14 int fcn(int); //是一个非虚函数,隐藏了D1::fcn(int)
15 int fcn(); //覆盖了Base的虚函数fcn
16 void f2(); //覆盖了D1的虚函数f2
17};
18
19Base bobj, D1 d1obj; D2 d2obj;
20
21Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
22bp1->fcn(); //虚调用,将在运行时调用Base::fcn
23bp2->fcn(); //虚调用,将在运行时调用Base::fcn
24bp3->fcn(); //虚调用,将在运行时调用D2::fcn
25
26D1 *bp1 = &bobj, *bp2 = &d1obj, *pb3 = &d2obj; //D1 *bp1 = &bobj根本就是直接报语法错误的
27bp2->f2(); //错误:Base没有名为f2的成员
28d1p->f2(); //虚调用,将在运行时调用D1::f2()
29d2p->f2(); //虚调用,将在运行时调用D1::f2()
类模板
类模板是用来生成类的蓝图的,与函数模板的不同之处是,编译器不能为类模板推断参数类型,如我们已经多次到的,为了使用类模板,我们必须在模板名后的尖括号中提供额外信息--用来代替模板参数的模板实参列表.

评论列表:
暂无评论 😭