- 编译器只会自动地进行一步类型转换。
第二章
算式类型有:int ,char,double,short等。string不是算术类型。
字面值类型有:算术类型,引用,指针,还有某些类(第七章介绍)等。
文件结束符,Windows是 Ctrl+Z,UNIX中是Ctrl + D
C++是一种静态数据类型语言,它的类型检查发生在编译时。
使用int而不是short,使用double而不是float。
给无符号类型一个超出大小的值,则结果是取模后的余数,给有符号一个超出大小的值,结果是未定义的。
如果表达式中既有带符号类型又有无符号类型,当带符号类型取值为负时会出现异常,因为带符号数会自动转换为无符号数。
用花括号 {} 进行初始化,称为列表初始化。
面对一条比较复杂的指针或者引用的声明语句时,从右向左阅读更有利于弄清楚它的真实含义。
int i = 0; int *p=&i; int *&r=p;//从右往左读,r是一个对指针的引用。
离变量名最近的符号对变量类型有最直接的影响(这里&说明r是引用),声明符的其余部分用以确定r引用的类型是什么。
double val = 1.01; const int &ri = val;//合法 int & ri = val;//非法
P55
弄懂一些声明的行之有效的方法是从右往左读,P56
用名词 顶层const 表示指针本身是一个常量,而用名词 底层const 表示指针所指的对象是一个常量
更一般的,顶层const可以表示任意对象是常量,这一点对任何数据类型都使用
用于声明引用的const都是底层const
const int &r=ci;
当执行对象的拷贝操作时,顶层const不受影响,底层的const却不能忽略。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能转换,比如说 非常量可以转换为常量,反之则不行。
常量表达式 是指值不会发生改变并且在编译阶段就能得到计算结果的表达式。
const int a=10;//a 是常量表达式 const int b=a+1;//b 是 int c = 27;// c 不是,因为它是个普通int而不是const int const int d = get_size();//d不是,尽管它本身是一个常量,但是它的具体值要到运算时才知道。
constexpr
有时候,我们定义一个const常量并把其初始值设定为常量表达式,但是有时候初始值是不确定的,可能并不是常量表达式。C++允许将变量定义为 constexpr类型以便由编译器来验证变量是否为一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。
意思就是一个常量表达式不一定能被初始化为常量,但是我们可以让编译器帮忙,检验出该常量表达式没被初始化成常量时,会报错。
constexpr int sz = size();//只有size是一个constexpr时才是一条正确语句。
不能用普通函数作为constexpr变量的初始值,但是允许定义一种constexpr函数,这种函数应该足够简单以使编译时就能计算其结果,这样就可以用constexpr函数来初始化constexpr变量了。
在constexpr声明中如果定义了一个指针,那么限定符constexpr仅对指针有效,与指针所指对象无关。即constexpr相当于定义了一个常量指针(即顶层指针)
auto一般会忽略顶层const,底层const会保留下来。
对常量对象取地址是一种底层const.
如果希望推出的auto是一个顶层const,需要明确指出:
const auto f =ci;
还可以将引用的类型设为auto,此时原来的初始化规则仍然适用
auto &g = ci; auto &h = 42;//错,不能为非常量引用绑定到字面值 const auto &i = 42;//d
decltype和引用
如果表达式的内容是解引用操作下,则decltype将得到引用类型。
decltype和auto的另一处重要区别,这里强烈建议看P63.
第三章
头文件不应该包含using声明
直接拷贝和初始化拷贝
如果使用等号初始化一个变量,实际上执行的是拷贝初始化,编译器把等号右边的初始值拷贝到新创建的对象去。与之相反,不适应等号则是直接初始化,。
getline(is,s) 从is中读取一行赋给s,返回is。
使用 >> 运算符给string对象输入,会自动忽略开头处的空白(即空格符,换行符,制表符等)
如果想保留下来,应该使用getline函数。其遇到换行符,就会停止并把读到的内容存入到那个string里去,但注意,不存换行符。
string::size_type 类型,所有用于存放string类的size函数返回值的变量,都应该是这种类型。由于size函数返回的是一个无符号整型数,所以如果在表达式中混用带符号数和无符号数可能产生意想不到的结果。
例如:s.size()<n ,n是负数int,那么结果几乎都会是true,因为n会自动转换成很大的无符号值。
字面值和string相加
"hello",'a',//这种是字符串字面值和字符字面值
当把string对象和字面值混在一条语句中相加时,必须确保每个加法运算符(+)的两边至少有有一个string对象
string s4 = s1+"," ;//对 string s5="hello"+",";//错 string s6=s1+','+"world";//对 string s7="hello"+","+s2;//错
切记,string和字符串字面值是不同的类型!!!
const_iterator和const iterator
const_iterator相当于 指针常量,其可以改变指向,但是指向的位置的值不能改变。
const iterator相当于 常量指针,其指向不能改变,但是指向的位置的值可以改变。
为了方便得到const_iterator类型的返回值,C++11引入了两个函数分别是:cbegin()和cend()。
箭头运算符
//it 是vector<string>的一个迭代器 (*it).empty()等价于 it->empty;
谨记,凡是使用了迭代器的循环体,都不要向迭代器所属容器添加元素。
数组不允许拷贝和赋值
int a[]={0,1,2}; int a2[]=a;//错误,不允许使用一个数组初始化另一个数组 a2=a;//错误,不允许将数组直接赋值给另一个数组
理解复杂的数组声明
最好的方法是,从名字开始,按照从内向外的顺序阅读。
int *(&arry) [10] = ptrs;
先看名字,&arry,所以是引用,看右边 [10],所以引用的对象是一个含有10个元素的数组,再看左边,数组存放的是整型指针类型。
这样,arry就是一个 含有10个int 指针的数组的引用。
int (*Parray)[10]= &arr;
看名字,是一个指针,看右边,其指向的是一个有十个元素的数组,看左边,数组保存的元素是整型。
int *ptrs[10];
看名字,是一个数组,看右边,有10个元素,看左边,保存的元素是int指针。
标准库函数begin和end
这两个函数与容器中的两个同名成员功能类似,但是其使用,是以数组为参数
int *beg=begin(ia); int *ed=end(ia);//指向ia尾元素的下一个位置
数组有一个特性,在很多用到数组名字的地方,编译器都会自动将其替换为一个指向数组首元素的指针。
第四章
显示类型转换
static_cast
任何具有明确定义的类型转换,只要不包含底层const,都可以使用、static_cast。
const_cast
const_cast只能改变运算对象的底层const,我们一般称之为”去掉const性质“。也只有const能改变表达式的常量属性。
**注意:**如果对象本身不是一个常量,使用强制类型转换获得读写权限是合法的,但是如果对象是一个常量,再使用const_cast执行写操作就会产生未定义的后果。
const char *cp; //明显是个底层const
const char *q=static_cast<char*>(cp);//错误,static_cast不能转换掉const性质
char *q = static_cast<string>(cp);//正确,字符串字面值转换为string
const_cast<string>(cp);//错误,const_cast只能改变底层const属性
第五章
这条语句
string s; while(cin>>s) ; cout<<s<<endl; //依次输入 hello hi ctrl+Z //最终结果是 hi
因此,每一次输入都是会刷新s。
第六章
形参的名字可以省略。
- 函数返回值不能是数组或函数,但可以是指向数组的指针和指向函数的指针。
局部静态对象
某些时候,有必要令局部变量的生命周期贯穿函数调用及之后的时间。可以将局部变量定义成static类型从而获得这样的对象。局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。
参数传递
当用实参初始化形参时会忽略顶层const
int func(int * const p);
int func(int *p);
将会出错,重复定义。
- 尽量使用常量引用
返回值
- 不要返回局部对象的引用和指针
函数重载
- 一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来,因为实参初始化形参时,顶层const会被忽略。
特殊用途语言特征
三种函数相关的语言特征:
- 默认实参
- 内联函数
- constexpr
默认实参
多次声明同一个函数是合法的,但是,在给定的作用域中,一个形参只能被赋予一次默认实参。后续声明只能为之前没有默认值的形参添加默认实参,而且该形参右侧的所有形参都必须有默认值
typedef string::size_type sz;
string screen(sz,sz,char = '');
string screen(sz,sz,char = '*');//错误,重复声明
string screen(sz,sz=80,char);//正确,添加默认实参
- 局部变量不能作为默认实参。只要表达式的类型能转换为形参所需的类型,该表达式就可以作为默认实参。
内联函数
一个简单的函数它可能有一些好处,但是调用它会比求等价的表达式的值要慢一些。而内联函数可以避免函数调用的开销。
在函数返回值前面加上 inline 可以将函数声明为内联函数了。
注意:内联要求只是向编译器发出的要求,编译器可以选择忽略这个要求。简单的函数才适合声明为内联函数。
PS:定义在类内的函数是隐式的inline(内联)函数。
constexpr函数
在函数返回值前面加上constexpr声明为constexpr函数
constexpr函数是指能用于常量表达式的函数。定义constexpr函数要遵循几个约定:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体必须有且只有一条return语句。
constexpr函数被隐式地指定为内联函数。
constexpr函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作就行。
需要注意的是 constexpr函数不一定返回常量表达式。所以用constexpr函数给constexpr变量初始化的语句还是有可能出错,由编译器检查。
调试帮助
assert预处理宏
assert是一种预处理宏,定义在cassert头文件中。
assert(expr);
首先对expr求值,如果表达式为假,assert输出信息并终止程序执行。如果为真,assert什么也不做。assert宏 常用于检查“不能发生”的条件。
NDEBUG预处理变量
assert设定行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG (其实意思就是NO DEBUG),则assert什么也不做。默认情况下没有定义NDEBUG。
#define NDEBUG
除了assert,还可以使用NDEBUG编写自己的条件调试代码。如果NDEBUG未定义,将执行#ifndef和#endif之间的代码,如果定义了,这些代码将被忽略。(ifndef相当于 if no def)
函数指针
函数指针指向某种特定类型。函数的类型与它的返回类型和形参类型共同决定。
指针函数声明
如同 复杂数组与指针的声明 类似,先看名字,再看左右边
bool (*pf) (const string & , const string &);//未初始化
先看名字,(*pf) 说明pf是个指针,右边是形参列表,说明指向的是个函数,看左边,返回值是bool类型。
bool * pf (const string & , const string &);// pf是一个返回bool指针的函数。
使用函数指针
当我们把函数名当成一个值使用时,该函数自动转换为指针。
bool lengthCompare(const string & , const string &);
pf=lengthCompare;
pf=&lengthCompare;//等价的
此外,可以使用函数指针直接调用函数而无需解引用:
bool b1=pf("hello","goodbye");
bool b1=(*pf)("hello","goodbye");
bool b1=lengthCompare("hello","goodbye");
指向不同函数类型的指针之间不存在转换规则。
函数重载与指针
编译器通过指针类型选用函数,指针类型必须与重载函数中的某一个精准匹配。
函数指针形参(重点)
形参可以是指向函数的指针。此时,形参看起来是 函数类型,实际上却是当成指针来使用:
void useBiggger(const string &s1,const string &s2,bool pf(const string &,const string &));//与下面等价
void useBiggger(const string &s1,const string &s2,bool (*pf)(const string &,const string &));
我们可以直接把函数作为实参使用,此时它自动转换为指针
useBigger(s1,s2,lengthCompare);
我们可以使用 typedef定义自己的类型。
typedef bool Func(const string &,const string &);
typedef decltype(lengthCompare) Func2;
typedef bool (*FuncP)(const string &,const string &);
typedef decltype(lengthCompare) FuncP2;
**注意区别:**Func和Func2是函数类型,Funcp和FuncP2是指针类型。
需要注意的是:decltype返回的是函数类型,其并不会如同函数形参那样把函数转换为 指针,所以只有在前面加上*才会得到指针。
void useBiggger(const string &s1,const string &s2,Func);
void useBiggger(const string &s1,const string &s2,FuncP2);
以上声明是同一个函数,因为第一条语句中,Func自动转换为指针。
第七章
定义在类内部的函数是隐式的inline函数。
引入this
成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。
在调用成员函数时,用请求该函数的对象 的 地址来初始化this。因为this的目的总是 指向这个对象,因此this是一个常量指针。
引入const成员函数
在函数的参数列表后面加一个 cosnt关键字,这里const的作用是修改隐式this指针的类型。
默认情况下,this的类型是指向类类型的 非常量版本的常量指针。也就是说,这样的this可以修改其指向对象的值(它自认为可以,因为它只是一个常量指针而不是指针常量)。但是,尽管this是隐式的,但它仍然需要遵循初始化规则,也就是说,默认情况下我们不能把this绑定到一个常量对象上。这一情况也就是使得我们不能在一个常量对象上调用普通的成员函数。
同时,因为this是隐式的,我们无法将其声明成 指针常量。所以我们的做法是,将const放在成员函数的参数列表之后,这样,紧跟在函数参数列表之后的const表示this是一个指向常量的指针。像这样使用const的成员函数我们称之为 常量成员函数。
类作用域与成员函数
不必考虑成员函数和数据成员的初始化先后顺序,因为编译器先编译成员的声明,然后才轮到成员函数体。
在类外定义成员函数
常量成员函数在类外的定义,需要和类内的声明保持一致。
构造函数
无论何时只要类的对象被创建,就会执行构造函数。
不同于其他函数(重点)
构造函数不能声明成const的。当我们创建类的一个const对象时,直到构造函数完成初始化过程,对象才能真正取得其常量属性。但是注意,构造函数体一旦开始执行,初始化就完成了,而非执行完构造函数才完成初始化。因此,我们无法在构造函数的函数体内进行给const数据成员赋值,只能通过 构造函数初始值(即初始值列表还有数据成员的类内初始值)来给const数据成员初始化。同理,const,引用,或者属于某种未提供默认构造函数的类类型也是如此。
合成构造函数
如果我们没有显示定义构造函数,编译器就会帮我们合成构造函数,称为 合成的默认构造函数
对于大多数类来说,这个合成的默认构造函数按一下规则来初始化类的数据成员:
- 如果存在类内的初始值,用它来初始化
- 否则,默认初始化。
某些类不能依赖合成的构造函数
- 编译器只有发现类不包含任何构造函数才会合成 默认构造函数
- 合成的默认构造函数可能执行错误的操作。如之前介绍过的,定义在块内的内置类型和复合类型被默认初始化,其值是未定义的。
- 有时编译器不能为某些类合成默认的构造函数,如类中包含一个其他类类型的成员且该成员的类型没有默认构造函数,那么编译器将无法初始化该成员。
=default的含义
Sales_data()=default;
要求编译器合成 默认构造函数。
没有出现在构造函数初始值列表的数据成员
将通过相应的类内初始值(如果存在的话)初始化,或者执行默认初始化),意思就是说,初始化的优先级上:
构造函数初始值列表>类内初始值>默认初始化
友元
友元声明只能出现在类定义的内部,但是在类内出现的位置不限,友元不是类的成员,不受它所在区域访问控制级别的约束。但一般来说,最好在类定义开始或结束前的位置集中声明友元。
类的其它特征
定义一个类型成员
public:
typedef std::string::size_type pos;
我们在public部分定义了pos,这样用户就可以使用这个名字。
需要注意的是:用来定义类型的成员必须先定义后使用,这一点和普通成员有区别。
令成员作为内联函数
类内部的成员函数默认为内联函数,但仍然可以用inline显示声明它
在声明和定义的地方同时声明 inline,这样是合法的,也是建议这么做的。
可变数据成员
有时(但不频繁)会发送这样一种情况,我们希望修改一个类的某个数据成员,即使在一个const成员函数内,即使它是const对象的成员。
可以通过在变量的声明中假如mutable关键字做到这点。
mutable size_t nums
返回 *this 的成员函数
一个const成员函数如果以引用的形式返回*this,那么它的返回类型将会是常量引用。
类的声明
class Screen;//类的声明
这种声明有时称为前向声明,在它声明之后定义之前是一个 不完全类型
不完全类型只能在非常有限的情景下使用:可以定义指向这种类型的指针和引用,也可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数。
因为只有当类全部完成后类才算被定义,所以一个类的成员类型不能是该类自己。然而一旦一个类的名字出现后,它就被认为是声明过了,因此类允许包含指向自身类型的指针或指针。
名字查找与类的作用域
类型名要特殊处理
一般来说,内层作用域可以重新定义外层作用域的名字,即使该名字已经在内层作用域使用过。但是在类中,如果成员使用了外层作用域中的某个名字,而该名字代表了一种类型,则类不能在之后重新定义该名字
typedef double money;
void func()
{
money m1;
typedef int money;//普通函数,允许。
}
typedef double money;
class Account{
public:
money balance(){return bal;}//已经使用了
private:
typedef double money;//错误,不能重新定义。即使其和前面的定义一模一样。
};
成员函数中使用名字的查找顺序
- 首先,在成员函数内查找
- 没找到,才会在类的定义找
- 如果类内没找到,才会在成员函数定义之前的作用域中查找。
class A
{
public:
void func(int height){return height*height;}//这里的height是形参height。
int height=0;
}
构造函数再探
构造函数的初始值有时必不可少
如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表来为这些成员提供初值。
成员初始化顺序
成员初始化的顺序与它们在类定义中的出现顺序一致。
委托构造函数
一个委托构造函数使用它所属的类的其他构造函数来完成自己的初始化过程,或者说它把它自己的一些或全部的职责委托给其它构造函数。
在委托构造函数内,成员初始值只有一个唯一的入口,就是类名本身。类名后紧跟圆括号括起来的参数列表,参数列表必须与类中另一个构造函数匹配。
Sales_data(int a,int b,string c):A(s),B(b),C(c){}
Sales_data(/*可以有参数*/):Sales_data(0,0,""){} //委托构造函数
如果受委托的构造函数的函数体内包含有代码的,先执行受委托函数的代码,然后控制权才会交还给委托者的函数体。
只允许一步类类型转换
编译器只会自动执行一步类型转换,例如如下的代码隐式地使用了两种转换规则:
combine(Sales_data &);
Sales_data::Sales_data(string );
item.combin("9-99-99");//错误,因为 "9-99-99"先转换为string,string再构造Salta_data;
可以改成
item.combin(string("9-99-99"));
抑制构造函数定义的隐式构造(重点)
我们可以通过在类内构造函数的声明前面 加上 explicit 关键字阻止隐式转换。注意只是阻止隐式的转换,显示的不阻止。
注意:关键字explicit只对一个实参的构造函数有效,需要多个实参的构造函数本身不能用于执行隐式转换,所以无需将其指定为explicit的。只能在类内声明时使用 explicit,在类外部定义时不应该重复。
explicit构造函数只能用于直接初始化
发生隐式转换的另一种情况是当我们执行拷贝形式的初始化时(使用=)。此时我们只能使用直接初始化而不能使用explicit函数
//假设这是类内的声明
explicit Sales_data(string );
Sales_data item(null_book);//正确,这是直接初始化。。null_book是一个string。
Sales_data item=null_book;//错误。
为转换显式地使用构造函数
item.combine(Sales_data(null_book));//显示构造一个对象
item.combine(static_cast<Sales_data>(null_book));//使用static_cast
聚合类
当一个类满足以下条件时,我们说它是聚合的:
- 所有成员都是public的;
- 没有定义任何构造函数;
- 没有类内初始值;
- 没有基类,也没有virtual函数。
字面值常量类
字面值类型的类可能含有 constexpr函数成员。这样的成员必须满足constexpr的所有要求,并且是隐式const的。
数据成员都是字面值类型的聚合类是字面值常量类,如果一个类满足以下条件,也是字面值常量类:
P267
类的静态成员
- 在成员的声明之前加上关键字static,声明为静态成员。在类外定义时,不能再加static,该关键字只能出现在类内的声明语句。(和explicit一样)。
- 类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。
- 静态成员函数不与任何对象绑定在一起,它们不包含this指针。因此,静态成员函数不能声明成const的,也不能在函数体内使用this指针。
使用类的静态成员
可以用作用域直接访问,也可以使用类的对象,引用或者指针来访问。成员函数则不需要通过作用域就能直接访问。、
定义静态成员
类外部定义,不能重复static关键字。不能在类内部定义,一个静态数据成员只能定义一次。
//interestRate是一个静态数据成员,initRate是同一个类内的函数
double Account::interestRate = initRate();
注意:以上语句从类名开始,剩余的部分语句就位于类的作用域之内了,因此我们可以直接使用initRate函数。
静态成员能用于某些场景,而普通成员不能。
举个例子,静态数据成员可以是不完全类型。特别的,静态数据成员的类型可以就是它所属的类类型。
class Bar
{
static Bar mem;//正确
Bar *mem2;//正确,指针类型
Bar mem3;//错误
};
另外一个区别就是我们可以用静态成员作为默认实参,而非静态成员不能作为默认实参,因为它的值本身属于对象的一部分,这么做的结果是无法真正提供一个对象以便从中获取成员的值,最终引发错误。