目录
前沿
第1章 开始
编译
初识输入输出
注释
控制流
第Ⅰ部分 C++基础
第2章 变量和基本类型
基本内置类型
变量
复合类型
const限定符
处理类型
自定义数据结构
第3章 字符串、向量和数组
命名空间的using声明
标准库类型string
前沿
第1章 开始
编译
-
从命令行运行编译器
-
$cc prog1.cc【$是系统提示符,cc是编程名字,windows下生成是prog1.exe;UNIX下生成的是a.out】
-
$ prog1【windows下省略.exe,UNIX下不可忽略如$a.out】
-
查看main返回值【win下$ echo %ERRORLEVEL%,UNIX下$ echo $】
-
运行GNU
-
$ g++ -0 prog1 prog1.cc【$是系统提示符。-o prog1是编译器参数,指定了可执行文件的文件名。在不同的操作系统中,此命令生成一个名为prog1或prog1.exe的可执行文件。在UNIX系统中,可执行文件没有后缀;在Windows系统中,后缀为.exe。如果省略了-o prog1参数,在UNIX系统中编译器会生成一个名为a.out的可执行文件,在Windows系统中则会生成一个名为a.exe的可执行文件】
-
运行VS
-
C:UsersmePrograms> cl /EHsc prog1.cpp【C:UsersmePrograms>是系统提示符,UsersmePrograms是当前目录名(即当前文件夹)。命令cl调用编译器,/EHsc是编译器选项,用来打开标准异常处理。微软编译器会自动生成一个可执行文件,其名字与第一个源文件名对应。可执行文件的文件名与源文件名相同,后缀为.exe。在此例中,可执行文件的文件名为prog1.exe】
初识输入输出
-
IO对象
-
cin、cout
-
cerr【标准错误。默认情况下,写到cerr的数据是不缓冲的。cerr通常用于输出错误信息或其他不属于程序正常逻辑的输出内容。】
-
clog【运行时的一般信息。默认情况下,写到clog的数据是被缓冲的。clog通常用于报告程序的执行信息,存入一个日志文件中。】
-
endl【写入endl的效果是结束当前行,并将与设备关联的缓冲区(buffer)中的内容刷到设备中。缓冲刷新操作可以保证到目前为止程序所产生的所有输出都真正写入输出流中,而不是仅停留在内存中等待写入流,序员常常在调试时添加打印语句。这类语句应该保证“一直”刷新流。否则,如果程序崩溃,输出可能还留在缓冲区中,从而导致关于程序崩溃位置的错误推断
注释
-
单行注释//这里是单行注释
-
多行注释
-
注释不能嵌套
*注释对不能嵌套。
*“不能嵌套”几个字会被认为是源码,```
控制流
-
控制流
-
读取数量不定的输入数据
#include
int main()
{
int sum=0,value=0;
//读取数据直到遇到文件尾,计算所有读入的值的和while(std::cin>>value)
sum+=value;//等价于sum=sum +value std::cout <<"Sum is:"<< sum<< std::endl;return 0;
}
此表达式从标准输入读取下一个数,保存在value中。输入运算符(参见1.2节,第7页)返回其左侧运算对象,在本例中是std::cin。因此,此循环条件实际上检测的是std::cin。
当我们使用一个istream对象作为条件时,其效果是检测流的状态。如果流是有效的,即流未遇到错误,那么检测成功。当遇到文件结束符(end-of-file),或遇到一个无效输入时(例如读入的值不是一个整数),istream对象的状态会变为无效。处于无效状态的istream对象会使条件变为假。
-
当从键盘向程序输入数据时,对于如何指出文件结束,不同操作系统有不同的约定。在Windows系统中,输入文件结束符的方法是敲Ctrl+Z(按住Ctrl键的同时按Z键),然后按Enter或Return键。在UNIX系统中,包括Mac OS X系统中,文件结束符输入是用Ctrl+D。
-
编译错误:语法错误、类型错误、声明错误
-
缓冲区【默认情况下,读cin会刷新cout;程序非正常终止时也会刷新cout。】
第Ⅰ部分 C++基础
第2章 变量和基本类型
基本内置类型
-
常用数据类型
常用数据类型 占用字节数 取值范围 short(前面可加unsigned)【整型】 2 -32768~32767(0~65535) int(前面可加unsigned)【整型】 4 -2147483648~2147483647(同理) long(前面可加unsigned)【整型】 4 -2147483648~2147483647(同理) long long(前面可加unsigned)【整型】 8 -9223372036854775808~…807(同理) float【实型】 4 负:-3.4×10^38~-3.4×10^-38 正:3.4×10^-38~3.4×10^38 double【实型】 8 负:-1.7×10^308~-1.7×10^-308 正:1.7×10^-308~1.7×10^308 long double【实型】 10 负:-1.1×10^4932~-1.1×10^-4932 正:1.1×10^-4932~1.1×10^4932 char(前面可加unsigned)【字符型】 1 -128~127(0~255) bool【布尔型】 1 true,false
-
类型转换
-
布尔类型:当我们把一个非布尔类型的算术值赋给布尔类型时,初始值为0则结果为false,否则结果为true。· 当我们把一个布尔值赋给非布尔类型时,初始值为false则结果为0,初始值为true则结果为1。
-
浮点数:当我们把一个浮点数赋给整数类型时,进行了近似处理。结果值将仅保留浮点数中小数点之前的部分。 当我们把一个整数值赋给浮点类型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失。
-
无符号:当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。例如,8比特大小的unsigned char可以表示0至255区间内的值,如果我们赋了一个区间以外的值,则实际的结果是该值对256取模后所得的余数。因此,把-1赋给8比特大小的unsigned char所得的结果是255。
-
字符:当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据。
-
当一个算术表达式中既有无符号数又有int值时,那个int值就会转换成无符号数
-
规则
-
隐式转换
高double⬅float
unsigned long int
long int
unsigned int
低int⬅char、short、bool
-
算术运算中转换:低到高
-
赋值运算中转换:
-
高向低:int a;a=15.5;
-
低向高:double a=10;cout<
-
显式转换:(数据类型)变量或常量、或数据类型(变量或常量或表达式)
-
常量
-
整型常量
-
二进制:0b10或者0B10
-
八进制:073
-
十进制:287
-
十八进制:0x1AF或者0x1AF
-
注意:cout << 1'100'000;分隔符不影响数值
-
实型常量
-
浮点表示法:-0.126
-
科学计数法:12.6E-5、126e-6→格式化:1.26E-4
-
字符常量
-
普通字符:‘A’(ASCII为65)
-
转义字符:
转义字符 功能 转义字符 功能 a 响铃 v 垂直制表 b 退格 ' f 换页 '' n 换行 \ r 回车(本行开头) nnn 1~3位八进制代表代表字符 t 水平制表,tab位置 xnn 十六进制代表队字符
注意:如果反斜线后面跟着的八进制数字超过3个,只有前3个数字与构成转义序列。例如,"1234"表示2个字符,即八进制数123对应的字符以及字符4。相反,x要用到后面跟着的所有数字,例如,"x1234"表示一个16位的字符,该字符由这4个十六进制数所对应的比特唯一确定。因为大多数机器的char型数据占8位,所以上面这个例子可能会报错。
-
字符串常量:
-
”Motnth“,长度为5,系统自动后面加‘ ’
-
string s = R"(zeng\mei)"; // 使用R"()"表示的原生字符串
-
指定字面值的类型
L’a'//宽字符型字面值,类型是wchar_t
u8"hi!"//utf-8字符串字面值(utf-8用8位编码一个Unicode字符)
42ULL//无符号整型字面值,类型是unsigned long 1ong
1E-3F//单精度浮点型字面值,类型是float
3.14159L//扩展精度浮点型字面值,类型是long double
变量
-
列表初始化
-
int units sold=0; int units sold={0}; int units sold{0};int units sold(0);都可以初始化和赋新值
-
当用于内置类型的变量时,这种初始化形式有一个重要特点:如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错:
long double 1d=3.1415926536;
int a{ld},b={1d};//错误:转换未执行,因为存在丢失信息的危险
intc(1d),d=1d;//正确:转换执行,且确实丢失了部分值 -
默认初始值
-
如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。定义于任何函数体之外的变量被初始化为0。
-
定义于函数体内的内置类型的对象如果没有初始化,则其值未定义。类的对象如果没有显式地初始化,则其值由类确定。
-
如果试图拷贝或以其他形式访问此类值将引发错误
-
变量声明和定义
-
如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显式地初始化变量:
extern int i;//声明i而非定义i
int j;//声明并定义
-
extern语句如果包含初始值就不再是声明,而变成定义了:
extern double pi=3.1416;//定义
-
变量能且只能被定义一次,但是可以被多次声明。所以,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。
-
标识符
-
用户自定义的标识符中不能连续出现两个下画线,也不能以下画线紧连大写字母开头。此外,定义在函数体外的标识符不能以下画线开头。
-
变量命名规范:
-
变量名一般用小写字母,如index,不要使用Index或INDEX
-
用户自定义的类名一般以大写字母开头,如Sales_item
-
如果标识符由多个单词组成,则单词间应有明显区分,如student_loan或studentLoan,不要使用studentloan
-
如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量
复合类型
-
引用(左值引用)
-
引用即别名:一般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值绑定(bind)在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。
int ival=1024;
int &refVal=ival;//refVal指向ival(是ival的另一个名字)
int &refVal2;//报错:引用必须被初始化
-
定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的:
refVal=2;//把2赋给refVal指向的对象,此处即是赋给了ival
int ii=refVal;//与ii=ival执行结果一样
-
因为引用本身不是一个对象,所以不能定义引用的引用
-
允许在一条语句中定义多个引用,其中每个引用标识符都必须以符号&开头:
int i=1024,i2=2048;//i和i2都是int
int &r=i,r2=i2;//r是一个引用,与i绑定在一起,r2是int
int i3=1024,&ri=i3;//i3是int,ri是一个引用,与i3绑定在一起
int &r3=i3,&r4=i2;//r3和r4都是引用
-
指针
-
指针与引用相比有很多不同点。其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。其二,指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
-
如果在一条语句中定义了几个指针变量,每个变量前面都必须有符号*
int*ipl,*ip2;//ipl和ip2都是指向int型对象的指针
double dp,*dp2;//dp2是指向double型对象的指针,dp是double型对象
-
因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。
-
空指针:
-
空指针形成方法:
int*pl=nullptr;//等价于int*pl=0;
int*p2=0;//直接将p2初始化为字面常量0
//需要首先#include cstdlib
int*p3=NULL;//等价于int*p3=0;
-
建议:初始化所有指针,如果实在不清楚指针应该指向何处,就把它初始化为nullptr或者0,这样程序就能检测并知道它没有指向任何具体的对象了
-
void *指针:是一种特殊的指针类型,可用于存放任意对象的地址。利用void*指针能做的事儿比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void*指针。不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。概括说来,以void*的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象。
-
理解复合类型的声明
-
一条定义语句可能定义出不同类型的变量:
//i是一个int型的数,p是一个int型指针,r是一个int型引用
int i=1024,*p=&i,&r=i;
-
int* p;//合法但是容易产生误导
我们说这种写法可能产生误导是因为int*放在一起好像是这条语句中所有变量共同的类型一样。其实恰恰相反,基本数据类型是int而非int*。*仅仅是修饰了p而已,对该声明语句中的其他变量,它并不产生任何作用:
int* pl,p2;//p1是指向int的指针,p2是int
-
指向指针的指针
int ival=1024;
int *pi=&ival;//pi指向一个int型的数
int **ppi=&pi;//ppi指向一个int型的指针
-
引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用:
int i=42;
int*p;//p是一个int型指针
int *&r=p;//r是一个对指针p的引用
r=&i;//r引用了一个指针,因此给r赋值&i就是令p指向i
*r=0;//解引用r得到i,也就是p指向的对象,将i的值改为0
要理解r的类型到底是什么,最简单的办法是从右向左阅读r的定义。离变量名最近的符号(此例中是&r的符号&)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针。最后,声明的基本数据类型部分指出r引用的是一个int指针
const限定符
-
初始化
-
因为const对象一旦创建后其值就不能再改变,所以const对象必须初始化
const inti=get size();//正确:运行时初始化
const intj=42;//正确:编译时初始化
const int k;//错误:k是一个未经初始化的常量
-
同时避免对同一变量的重复定义,默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量
-
如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字:
//file1.cc定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufsize=fcn();
//file1.h头文件
extern const int bufsize;//与file 1.cc中定义的bufsize是同一个
-
const的引用
-
const int ci=1024;
const int &rl=ci;//正确:引用及其对应的对象都是常量
r1=42;//错误:r1是对常量的引用
int &r2=ci;//错误:试图让一个非常量引用指向一个常量对象
-
C++程序员们经常把词组“对const的引用”简称为“常量引用”,这一简称还是挺靠谱的,不过前提是你得时刻记得这就是个简称而已。严格来说,并不存在常量引用。因为引用不是一个对象,所以我们没法让引用本身恒定不变。事实上,由于C++语言并不允许随意改变引用所绑定的对象,所以从这层意义上理解所有的引用又都算是常量。
-
引用的类型必须与其所引用对象的类型一致,但是有两个例外。第一种例外情况就是在初始化常量引用时允许用任意表达式作为初始值(临时常量=i;常量引用=;临时常量)
int i=42;
const int&rl=i;//允许将const ints绑定到一个普通int对象上
const int&r2=42;//正确:r1是一个常量引用
const int&r3=r1*2;//正确:r3是一个常量引用
int &r4=r1*2;//错误:r4是一个普通的非常量引用
r2绑定(非常量)整数i是合法的行为。然而,不允许通过r2修改i的值。尽管如此,i的值仍然允许通过其他途径修改,既可以直接给i赋值,也可以通过绑定到i的其他引用来修改
-
const double pi=3.14;//pi是个常量,它的值不能改变
double *ptr=&pi;//错误:ptr是一个普通指针
const double*cptr=&pi;//正确:cptr可以指向一个双精度常量
*Cptr=42;//错误:不能给*cptr赋值
-
指针的类型必须与其所指对象的类型一致,但是有两个例外。第一种例外情况是允许令一个指向常量的指针指向一个非常量对象。和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变
-
指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定为常量。常量指针(const pointer)必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。把*放在const关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味,即不变的是指针本身的值而非指向的那个值:
int errNumb=0;
int *const curErr=&errNumb;//curErr将一直指向errNumb
const double pi=3.14159;
const double *const pip=&pi;//pip是一个指向常量对象的常量指针
可以通过curErr修改errNumb值,不可以通过pip改变pi
-
顶层const和底层const
-
用名词顶层const(top-level const)表示指针本身是个常量,而用名词底层const(low-level const)表示指针所指的对象是一个常量
inti=0;
int *const pl=&i;//不能改变p1的值,这是一个顶层const
const int ci=42;//不能改变ci的值,这是一个顶层const
const int*p2=&ci;//允许改变p2的值,这是一个底层const
const int*const p3=p2;//靠右的 const 是顶层const,靠左的是底层const
const int&r=ci;//用于声明引用的 const 都是底层const
-
当执行对象的拷贝操作时,常量是顶层const还是底层const区别明显。其中,顶层const不受什么影响:
i=ci;//正确:拷贝ci的值,ci是一个顶层const,对此操作无影响
p2=p3;//正确:p2和p3指向的对象类型相同,p3顶层const的部分不影响
-
底层const的限制却不能忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之则不行:
int *p=p3;//错误:p3包含底层const的定义,而p没有
p2=p3;//正确:p2和p3都是底层const
p2=&i;//正确:int*能转换成const int*
int &r=ci;//错误:普通的int&不能绑定到int常量上
const int&r2=i;//正确:const int&可以绑定到一个普通int上
p3既是顶层const也是底层const,拷贝p3时可以不在乎它是一个顶层const,但是必须清楚它指向的对象得是一个常量。因此,不能用p3去初始化p,因为p指向的是一个普通的(非常量)整数。另一方面,p3的值可以赋给p2,是因为这两个指针都是底层const,尽管p3同时也是一个常量指针(顶层const),仅就这次赋值而言不会有什么影响
-
常量表达式
-
常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式
const int max files=20;//max files是常量表达式
const int limit=max files+1;//limit是常量表达式
int staff_size=27;//staff_size不是常量表达式
const int sz=get_size();//sz不是常量表达式
尽管sz本身是一个常量,但它的具体值直到运行时才能获取到,所以不是常量表达式
-
C++11 标准中,定义变量时可以用 constexpr 修饰,从而使该变量获得在编译阶段即可计算出结果的能力。常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为“字面值类型”。算术类型、引用和指针都属于字面值类型。自定义类Sales_item、IO库、string类型则不属于字面值类型,也就不能被定义成constexpr。尽管指针和引用都能定义成constexpr,但它们的初始值却受到严格限制。一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象。(函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。同样是在6.1.1节(第185页)中还将提到,允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址。因此,constexpr引用能绑定到这样的变量上,constexpr指针也能指向这样的变量)新标准允许定义一种特殊的constexpr函数。这种函数应该足够简单以使得编译时就可以计算其结果,这样就能用constexpr函数去初始化constexpr变量了。
处理类型
-
类型别名
-
方法一:
typedef double wages;//wages是double的同义词
typedef wages base,*p;//base是double的同义词,p是double*的同义词
-
方法二:
using SI=Sales item;//SI是Sales item的同义词
-
typedef char *pstring;
const pstring cstr=0;//cstr是指向char的常量指针
const pstring *ps;//ps是一个指针,它的对象是指向char的常量指针
char*别名是pstring
const char *cstr=0;//是对const pstring cstr的错误理解
声明语句中用到pstring时,其基本数据类型是指针。可是用char*重写了声明语句后,数据类型就变成了char,*成为了声明符的一部分。这样改写的结果是,const char成了基本数据类型。前后两种声明含义截然不同,前者声明了一个指向char的常量指针,改写后的形式则声明了一个指向const char的指针。
-
auto
-
C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。显然,auto定义的变量必须有初始值。
-
使用auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:
auto i=0,*p=&i;//正确:i是整数、p是整型指针
auto sz=0,pi=3.14;//错误:sz和pi的类型不一致
-
auto一般会忽略掉顶层const(参见2.4.3节,第57页),同时底层const则会保留下来,比如当初始值是一个指向常量的指针时:
const int ci=i,&cr=ci;
auto b=ci;//b是一个整数(ci的顶层const特性被忽略掉了)
auto c=cr;//c是一个整数(cr是ci的别名,ci本身是一个顶层const))
auto d=&i;//d是一个整型指针(整数的地址就是指向整数的指针)
auto e=&ci;//e是一个指向整数常量的指针(对常量对象取地址是一种底层const)
如果希望推断出的auto类型是一个顶层const,需要明确指出:
const auto f=ci;//ci的推演类型是int,f是const int
-
引用
auto &g=ci;//g是一个整型常量引用,绑定到ci
auto &h=42;//错误:不能为非常量引用绑定字面值
const auto &j=42;//正确:可以为常量引用绑定字面值
设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留。和往常一样,如果我们给初始值绑定一个引用,则此时的常量就不是顶层常量了
auto k=ci,&l=i;//k是整数,1是整型引用
auto &m=ci,*p=&ci;//m是对整型常量的引用,p是指向整型常量的指针
//错误:i的类型是int而&ci的类型是const int
auto n=i,*p2=&ci;
-
decltype
-
如果我们仅仅是想根据初始值确定一个变量合适的数据类型,那么auto是最佳人选。而只有当我们需要推断某个表达式的数据类型,并将其作为一种新的数据类型重复使用(比如,定义多个相同类型变量)或者单独使用(比如,作为函数的返回值类型)时,我们才真正需要用到decltype。
-
decltype(f())sum=x;//sum的类型就是函数f的返回类型
编译器并不实际调用函数f,而是使用当调用发生时f的返回值类型作为sum的类型。
-
如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内):
const int ci=0,&cj=ci;
decltype(ci)x=0;//x的类型是const int
decltype(cj)y=x;//y的类型是const int&,y绑定到变量x
decltype(cj)z;//错误:z是一个引用,必须初始化
因为cj是一个引用,decltype(cj)的结果就是引用类型,因此作为引用的z必须被初始化。需要指出的是,引用从来都作为其所指对象的同义词出现,只有用在decltype处是一个例外。
如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。
//decltype的结果可以是引用类型
int i=42,*p=&i,&r=i;
decltype(r+0)b;//正确:加法的结果是int,因此b是一个(未初始化的)int
decltype(*p)c;//错误:c是int&,必须初始化
因为r是一个引用,因此decltype(r)的结果是引用类型。如果想让结果类型是r所指的类型,可以把r作为表达式的一部分,如r+0,显然这个表达式的结果将是一个具体值而非一个引用。另一方面,如果表达式的内容是解引用操作,则decltype将得到引用类型。正如我们所熟悉的那样,解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype(*p)的结果类型就是int&,而非int。
-
//decltype的表达式如果是加上了括号的变量,结果将是引用
decltype((i))d;//错误:d是int&,必须初始化
decltype(i)e;//正确:e是一个(未初始化的)int
decltype((variable))(注意是双层括号)的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用
自定义数据结构
-
定义
-
struct Sales data{}accum,trans,*salesptr;
//与上一条语句等价,但可能更好一些
struct Sales data{};
Sales data accum,trans,*salesptr; 分号表示声明符(通常为空)的结束。一般来说,最好不要把对象的定义和类的定义放在一起。这么做无异于把两种不同实体的定义混在了一条语句里,一会儿定义类,一会儿又定义变量,显然这是一种不被建议的行为。
-
如果一个指针指向的是一个结构体类型的变量,与结构体变量使用“.”符号引出成员变量不同的是,如果是指向结构体的指针,则应该用“->”符号引出其成员变量。
// 定义一个结构体变量
Employee Zengmei;
// 定义一个指针,并将其指向这个结构体变量
Employee* pZengmei = &Zengmei;
// 用“->”运算符,引用这个结构体指针变量的成员变量
pZengmei->m_strName = "Zengmei";
pZengmei->m_nAge = 28;
-
编写头文件
-
类一般都不定义在函数体内。当在函数体外部定义类时,在各个指定的源文件中可能只有一处该类的定义。而且,如果要在不同文件中使用同一个类,类的定义就必须保持一致。为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样。例如,库类型string在名为string的头文件中定义。又如,我们应该把Sales_data类定义在名为Sales_data.h的头文件中。头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明。
-
当预处理器看到#include标记时就会用指定的头文件的内容代替#include。C++程序还会用到的一项预处理功能是头文件保护符(headerguard),头文件保护符依赖于预处理变量(参见2.3.2节,第48页)。预处理变量有两种状态:已定义和未定义。#define指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:#ifdef当且仅当变量已定义时为真,#ifndef当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到#endif指令为止。使用这些功能就能有效地防止重复包含的发生:
#ifndef SALES DATA H
#define SALES DATA H
#include
struct Sales data{
std::string bookNo;
unsigned units sold=0;
double revenue=0.0;
};
#endif
第一次包含Sales_data.h时,#ifndef的检查结果为真,预处理器将顺序执行后面的操作直至遇到#endif为止。此时,预处理变量SALES_DATA_H的值将变为已定义,而且Sales_data.h也会被拷贝到我们的程序中来。后面如果再一次包含Sales_data.h,则#ifndef的检查结果将为假,编译器将忽略#ifndef到#endif之间的部分。
第3章 字符串、向量和数组
命名空间的using声明
-
std::cin的意思就是要使用命名空间std中的名字cin
-
using namespace::name;
using std::cin;
cin>>i;//正确:cin和std::cin含义相同
-
位于头文件的代码一般来说不应该使用using声明。这是因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个using声明,那么每个使用了该头文件的文件就都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。
标准库类型string
-
使用string类型必须首先包含string头文件
-
初始化
string s1;//默认初始化,s1是一个空字符串
string s2=s1;//s2是s1的副本
string s3="hiya";//s3是该字符串字面值的副本
tring s4(10,'c');//s4的内容是cccccccccc
string s1默认初始化,s1是一个空串
string s2(s1)s2是s1的副本
string s2=s1等价于s2(s1),s2是s1的副本
string s3("value")s3是字面值"value”的副本,除了字面值最后的那个空字符外
string s3="value”等价于s3("value"),s3是字面值“value”的副本
string s4(n,'c')把s4初始化为由连续n个字符c组成的串
-
直接初始化和拷贝初始化
-
当初始值只有一个时,使用直接初始化或拷贝初始化都行。如果像上面的s4那样初始化要用到的值有多个,一般来说只能使用直接初始化的方式
string s5="hiya";//拷贝初始化
string s6("hiya");//直接初始化
string s7(10,'c');//直接初始化,s7的内容是cccccccccc
-
对于用多个值进行初始化的情况,非要用拷贝初始化的方式来处理也不是不可以,不过需要显式地创建一个(临时)对象用于拷贝
string s8=string(10,'c');//拷贝初始化,s8的内容是cccccccccc
编译
-
从命令行运行编译器
-
$cc prog1.cc【$是系统提示符,cc是编程名字,windows下生成是prog1.exe;UNIX下生成的是a.out】
-
$ prog1【windows下省略.exe,UNIX下不可忽略如$a.out】
-
查看main返回值【win下$ echo %ERRORLEVEL%,UNIX下$ echo $】
-
运行GNU
-
$ g++ -0 prog1 prog1.cc【$是系统提示符。-o prog1是编译器参数,指定了可执行文件的文件名。在不同的操作系统中,此命令生成一个名为prog1或prog1.exe的可执行文件。在UNIX系统中,可执行文件没有后缀;在Windows系统中,后缀为.exe。如果省略了-o prog1参数,在UNIX系统中编译器会生成一个名为a.out的可执行文件,在Windows系统中则会生成一个名为a.exe的可执行文件】
-
运行VS
-
C:UsersmePrograms> cl /EHsc prog1.cpp【C:UsersmePrograms>是系统提示符,UsersmePrograms是当前目录名(即当前文件夹)。命令cl调用编译器,/EHsc是编译器选项,用来打开标准异常处理。微软编译器会自动生成一个可执行文件,其名字与第一个源文件名对应。可执行文件的文件名与源文件名相同,后缀为.exe。在此例中,可执行文件的文件名为prog1.exe】
初识输入输出
-
IO对象
-
cin、cout
-
cerr【标准错误。默认情况下,写到cerr的数据是不缓冲的。cerr通常用于输出错误信息或其他不属于程序正常逻辑的输出内容。】
-
clog【运行时的一般信息。默认情况下,写到clog的数据是被缓冲的。clog通常用于报告程序的执行信息,存入一个日志文件中。】
-
endl【写入endl的效果是结束当前行,并将与设备关联的缓冲区(buffer)中的内容刷到设备中。缓冲刷新操作可以保证到目前为止程序所产生的所有输出都真正写入输出流中,而不是仅停留在内存中等待写入流,序员常常在调试时添加打印语句。这类语句应该保证“一直”刷新流。否则,如果程序崩溃,输出可能还留在缓冲区中,从而导致关于程序崩溃位置的错误推断
注释
-
单行注释//这里是单行注释
-
多行注释
-
注释不能嵌套
*注释对不能嵌套。
*“不能嵌套”几个字会被认为是源码,```
控制流
-
控制流
-
读取数量不定的输入数据
#include
int main()
{
int sum=0,value=0;
//读取数据直到遇到文件尾,计算所有读入的值的和while(std::cin>>value)
sum+=value;//等价于sum=sum +value std::cout <<"Sum is:"<< sum<< std::endl;return 0;
}
此表达式从标准输入读取下一个数,保存在value中。输入运算符(参见1.2节,第7页)返回其左侧运算对象,在本例中是std::cin。因此,此循环条件实际上检测的是std::cin。
当我们使用一个istream对象作为条件时,其效果是检测流的状态。如果流是有效的,即流未遇到错误,那么检测成功。当遇到文件结束符(end-of-file),或遇到一个无效输入时(例如读入的值不是一个整数),istream对象的状态会变为无效。处于无效状态的istream对象会使条件变为假。
-
当从键盘向程序输入数据时,对于如何指出文件结束,不同操作系统有不同的约定。在Windows系统中,输入文件结束符的方法是敲Ctrl+Z(按住Ctrl键的同时按Z键),然后按Enter或Return键。在UNIX系统中,包括Mac OS X系统中,文件结束符输入是用Ctrl+D。
-
编译错误:语法错误、类型错误、声明错误
-
缓冲区【默认情况下,读cin会刷新cout;程序非正常终止时也会刷新cout。】
第Ⅰ部分 C++基础
第2章 变量和基本类型
基本内置类型
-
常用数据类型
常用数据类型 占用字节数 取值范围 short(前面可加unsigned)【整型】 2 -32768~32767(0~65535) int(前面可加unsigned)【整型】 4 -2147483648~2147483647(同理) long(前面可加unsigned)【整型】 4 -2147483648~2147483647(同理) long long(前面可加unsigned)【整型】 8 -9223372036854775808~…807(同理) float【实型】 4 负:-3.4×10^38~-3.4×10^-38 正:3.4×10^-38~3.4×10^38 double【实型】 8 负:-1.7×10^308~-1.7×10^-308 正:1.7×10^-308~1.7×10^308 long double【实型】 10 负:-1.1×10^4932~-1.1×10^-4932 正:1.1×10^-4932~1.1×10^4932 char(前面可加unsigned)【字符型】 1 -128~127(0~255) bool【布尔型】 1 true,false
-
类型转换
-
布尔类型:当我们把一个非布尔类型的算术值赋给布尔类型时,初始值为0则结果为false,否则结果为true。· 当我们把一个布尔值赋给非布尔类型时,初始值为false则结果为0,初始值为true则结果为1。
-
浮点数:当我们把一个浮点数赋给整数类型时,进行了近似处理。结果值将仅保留浮点数中小数点之前的部分。 当我们把一个整数值赋给浮点类型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失。
-
无符号:当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。例如,8比特大小的unsigned char可以表示0至255区间内的值,如果我们赋了一个区间以外的值,则实际的结果是该值对256取模后所得的余数。因此,把-1赋给8比特大小的unsigned char所得的结果是255。
-
字符:当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据。
-
当一个算术表达式中既有无符号数又有int值时,那个int值就会转换成无符号数
-
规则
-
隐式转换
高double⬅float
unsigned long int
long int
unsigned int
低int⬅char、short、bool
-
算术运算中转换:低到高
-
赋值运算中转换:
-
高向低:int a;a=15.5;
-
低向高:double a=10;cout<
-
显式转换:(数据类型)变量或常量、或数据类型(变量或常量或表达式)
-
常量
-
整型常量
-
二进制:0b10或者0B10
-
八进制:073
-
十进制:287
-
十八进制:0x1AF或者0x1AF
-
注意:cout << 1'100'000;分隔符不影响数值
-
实型常量
-
浮点表示法:-0.126
-
科学计数法:12.6E-5、126e-6→格式化:1.26E-4
-
字符常量
-
普通字符:‘A’(ASCII为65)
-
转义字符:
转义字符 功能 转义字符 功能 a 响铃 v 垂直制表 b 退格 ' f 换页 '' n 换行 \ r 回车(本行开头) nnn 1~3位八进制代表代表字符 t 水平制表,tab位置 xnn 十六进制代表队字符
注意:如果反斜线后面跟着的八进制数字超过3个,只有前3个数字与构成转义序列。例如,"1234"表示2个字符,即八进制数123对应的字符以及字符4。相反,x要用到后面跟着的所有数字,例如,"x1234"表示一个16位的字符,该字符由这4个十六进制数所对应的比特唯一确定。因为大多数机器的char型数据占8位,所以上面这个例子可能会报错。
-
字符串常量:
-
”Motnth“,长度为5,系统自动后面加‘ ’
-
string s = R"(zeng\mei)"; // 使用R"()"表示的原生字符串
-
指定字面值的类型
L’a'//宽字符型字面值,类型是wchar_t
u8"hi!"//utf-8字符串字面值(utf-8用8位编码一个Unicode字符)
42ULL//无符号整型字面值,类型是unsigned long 1ong
1E-3F//单精度浮点型字面值,类型是float
3.14159L//扩展精度浮点型字面值,类型是long double
变量
-
列表初始化
-
int units sold=0; int units sold={0}; int units sold{0};int units sold(0);都可以初始化和赋新值
-
当用于内置类型的变量时,这种初始化形式有一个重要特点:如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错:
long double 1d=3.1415926536;
int a{ld},b={1d};//错误:转换未执行,因为存在丢失信息的危险
intc(1d),d=1d;//正确:转换执行,且确实丢失了部分值 -
默认初始值
-
如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。定义于任何函数体之外的变量被初始化为0。
-
定义于函数体内的内置类型的对象如果没有初始化,则其值未定义。类的对象如果没有显式地初始化,则其值由类确定。
-
如果试图拷贝或以其他形式访问此类值将引发错误
-
变量声明和定义
-
如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显式地初始化变量:
extern int i;//声明i而非定义i
int j;//声明并定义
-
extern语句如果包含初始值就不再是声明,而变成定义了:
extern double pi=3.1416;//定义
-
变量能且只能被定义一次,但是可以被多次声明。所以,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。
-
标识符
-
用户自定义的标识符中不能连续出现两个下画线,也不能以下画线紧连大写字母开头。此外,定义在函数体外的标识符不能以下画线开头。
-
变量命名规范:
-
变量名一般用小写字母,如index,不要使用Index或INDEX
-
用户自定义的类名一般以大写字母开头,如Sales_item
-
如果标识符由多个单词组成,则单词间应有明显区分,如student_loan或studentLoan,不要使用studentloan
-
如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量
复合类型
-
引用(左值引用)
-
引用即别名:一般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值绑定(bind)在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。
int ival=1024;
int &refVal=ival;//refVal指向ival(是ival的另一个名字)
int &refVal2;//报错:引用必须被初始化
-
定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的:
refVal=2;//把2赋给refVal指向的对象,此处即是赋给了ival
int ii=refVal;//与ii=ival执行结果一样
-
因为引用本身不是一个对象,所以不能定义引用的引用
-
允许在一条语句中定义多个引用,其中每个引用标识符都必须以符号&开头:
int i=1024,i2=2048;//i和i2都是int
int &r=i,r2=i2;//r是一个引用,与i绑定在一起,r2是int
int i3=1024,&ri=i3;//i3是int,ri是一个引用,与i3绑定在一起
int &r3=i3,&r4=i2;//r3和r4都是引用
-
指针
-
指针与引用相比有很多不同点。其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。其二,指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
-
如果在一条语句中定义了几个指针变量,每个变量前面都必须有符号*
int*ipl,*ip2;//ipl和ip2都是指向int型对象的指针
double dp,*dp2;//dp2是指向double型对象的指针,dp是double型对象
-
因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。
-
空指针:
-
空指针形成方法:
int*pl=nullptr;//等价于int*pl=0;
int*p2=0;//直接将p2初始化为字面常量0
//需要首先#include cstdlib
int*p3=NULL;//等价于int*p3=0;
-
建议:初始化所有指针,如果实在不清楚指针应该指向何处,就把它初始化为nullptr或者0,这样程序就能检测并知道它没有指向任何具体的对象了
-
void *指针:是一种特殊的指针类型,可用于存放任意对象的地址。利用void*指针能做的事儿比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void*指针。不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。概括说来,以void*的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象。
-
理解复合类型的声明
-
一条定义语句可能定义出不同类型的变量:
//i是一个int型的数,p是一个int型指针,r是一个int型引用
int i=1024,*p=&i,&r=i;
-
int* p;//合法但是容易产生误导
我们说这种写法可能产生误导是因为int*放在一起好像是这条语句中所有变量共同的类型一样。其实恰恰相反,基本数据类型是int而非int*。*仅仅是修饰了p而已,对该声明语句中的其他变量,它并不产生任何作用:
int* pl,p2;//p1是指向int的指针,p2是int
-
指向指针的指针
int ival=1024;
int *pi=&ival;//pi指向一个int型的数
int **ppi=&pi;//ppi指向一个int型的指针
-
引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用:
int i=42;
int*p;//p是一个int型指针
int *&r=p;//r是一个对指针p的引用
r=&i;//r引用了一个指针,因此给r赋值&i就是令p指向i
*r=0;//解引用r得到i,也就是p指向的对象,将i的值改为0
要理解r的类型到底是什么,最简单的办法是从右向左阅读r的定义。离变量名最近的符号(此例中是&r的符号&)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针。最后,声明的基本数据类型部分指出r引用的是一个int指针
const限定符
-
初始化
-
因为const对象一旦创建后其值就不能再改变,所以const对象必须初始化
const inti=get size();//正确:运行时初始化
const intj=42;//正确:编译时初始化
const int k;//错误:k是一个未经初始化的常量
-
同时避免对同一变量的重复定义,默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量
-
如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字:
//file1.cc定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufsize=fcn();
//file1.h头文件
extern const int bufsize;//与file 1.cc中定义的bufsize是同一个
-
const的引用
-
const int ci=1024;
const int &rl=ci;//正确:引用及其对应的对象都是常量
r1=42;//错误:r1是对常量的引用
int &r2=ci;//错误:试图让一个非常量引用指向一个常量对象
-
C++程序员们经常把词组“对const的引用”简称为“常量引用”,这一简称还是挺靠谱的,不过前提是你得时刻记得这就是个简称而已。严格来说,并不存在常量引用。因为引用不是一个对象,所以我们没法让引用本身恒定不变。事实上,由于C++语言并不允许随意改变引用所绑定的对象,所以从这层意义上理解所有的引用又都算是常量。
-
引用的类型必须与其所引用对象的类型一致,但是有两个例外。第一种例外情况就是在初始化常量引用时允许用任意表达式作为初始值(临时常量=i;常量引用=;临时常量)
int i=42;
const int&rl=i;//允许将const ints绑定到一个普通int对象上
const int&r2=42;//正确:r1是一个常量引用
const int&r3=r1*2;//正确:r3是一个常量引用
int &r4=r1*2;//错误:r4是一个普通的非常量引用
r2绑定(非常量)整数i是合法的行为。然而,不允许通过r2修改i的值。尽管如此,i的值仍然允许通过其他途径修改,既可以直接给i赋值,也可以通过绑定到i的其他引用来修改
-
const double pi=3.14;//pi是个常量,它的值不能改变
double *ptr=&pi;//错误:ptr是一个普通指针
const double*cptr=&pi;//正确:cptr可以指向一个双精度常量
*Cptr=42;//错误:不能给*cptr赋值
-
指针的类型必须与其所指对象的类型一致,但是有两个例外。第一种例外情况是允许令一个指向常量的指针指向一个非常量对象。和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变
-
指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定为常量。常量指针(const pointer)必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。把*放在const关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味,即不变的是指针本身的值而非指向的那个值:
int errNumb=0;
int *const curErr=&errNumb;//curErr将一直指向errNumb
const double pi=3.14159;
const double *const pip=&pi;//pip是一个指向常量对象的常量指针
可以通过curErr修改errNumb值,不可以通过pip改变pi
-
顶层const和底层const
-
用名词顶层const(top-level const)表示指针本身是个常量,而用名词底层const(low-level const)表示指针所指的对象是一个常量
inti=0;
int *const pl=&i;//不能改变p1的值,这是一个顶层const
const int ci=42;//不能改变ci的值,这是一个顶层const
const int*p2=&ci;//允许改变p2的值,这是一个底层const
const int*const p3=p2;//靠右的 const 是顶层const,靠左的是底层const
const int&r=ci;//用于声明引用的 const 都是底层const
-
当执行对象的拷贝操作时,常量是顶层const还是底层const区别明显。其中,顶层const不受什么影响:
i=ci;//正确:拷贝ci的值,ci是一个顶层const,对此操作无影响
p2=p3;//正确:p2和p3指向的对象类型相同,p3顶层const的部分不影响
-
底层const的限制却不能忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之则不行:
int *p=p3;//错误:p3包含底层const的定义,而p没有
p2=p3;//正确:p2和p3都是底层const
p2=&i;//正确:int*能转换成const int*
int &r=ci;//错误:普通的int&不能绑定到int常量上
const int&r2=i;//正确:const int&可以绑定到一个普通int上
p3既是顶层const也是底层const,拷贝p3时可以不在乎它是一个顶层const,但是必须清楚它指向的对象得是一个常量。因此,不能用p3去初始化p,因为p指向的是一个普通的(非常量)整数。另一方面,p3的值可以赋给p2,是因为这两个指针都是底层const,尽管p3同时也是一个常量指针(顶层const),仅就这次赋值而言不会有什么影响
-
常量表达式
-
常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式
const int max files=20;//max files是常量表达式
const int limit=max files+1;//limit是常量表达式
int staff_size=27;//staff_size不是常量表达式
const int sz=get_size();//sz不是常量表达式
尽管sz本身是一个常量,但它的具体值直到运行时才能获取到,所以不是常量表达式
-
C++11 标准中,定义变量时可以用 constexpr 修饰,从而使该变量获得在编译阶段即可计算出结果的能力。常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为“字面值类型”。算术类型、引用和指针都属于字面值类型。自定义类Sales_item、IO库、string类型则不属于字面值类型,也就不能被定义成constexpr。尽管指针和引用都能定义成constexpr,但它们的初始值却受到严格限制。一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象。(函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。同样是在6.1.1节(第185页)中还将提到,允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址。因此,constexpr引用能绑定到这样的变量上,constexpr指针也能指向这样的变量)新标准允许定义一种特殊的constexpr函数。这种函数应该足够简单以使得编译时就可以计算其结果,这样就能用constexpr函数去初始化constexpr变量了。
处理类型
-
类型别名
-
方法一:
typedef double wages;//wages是double的同义词
typedef wages base,*p;//base是double的同义词,p是double*的同义词
-
方法二:
using SI=Sales item;//SI是Sales item的同义词
-
typedef char *pstring;
const pstring cstr=0;//cstr是指向char的常量指针
const pstring *ps;//ps是一个指针,它的对象是指向char的常量指针
char*别名是pstring
const char *cstr=0;//是对const pstring cstr的错误理解
声明语句中用到pstring时,其基本数据类型是指针。可是用char*重写了声明语句后,数据类型就变成了char,*成为了声明符的一部分。这样改写的结果是,const char成了基本数据类型。前后两种声明含义截然不同,前者声明了一个指向char的常量指针,改写后的形式则声明了一个指向const char的指针。
-
auto
-
C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。显然,auto定义的变量必须有初始值。
-
使用auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:
auto i=0,*p=&i;//正确:i是整数、p是整型指针
auto sz=0,pi=3.14;//错误:sz和pi的类型不一致
-
auto一般会忽略掉顶层const(参见2.4.3节,第57页),同时底层const则会保留下来,比如当初始值是一个指向常量的指针时:
const int ci=i,&cr=ci;
auto b=ci;//b是一个整数(ci的顶层const特性被忽略掉了)
auto c=cr;//c是一个整数(cr是ci的别名,ci本身是一个顶层const))
auto d=&i;//d是一个整型指针(整数的地址就是指向整数的指针)
auto e=&ci;//e是一个指向整数常量的指针(对常量对象取地址是一种底层const)
如果希望推断出的auto类型是一个顶层const,需要明确指出:
const auto f=ci;//ci的推演类型是int,f是const int
-
引用
auto &g=ci;//g是一个整型常量引用,绑定到ci
auto &h=42;//错误:不能为非常量引用绑定字面值
const auto &j=42;//正确:可以为常量引用绑定字面值
设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留。和往常一样,如果我们给初始值绑定一个引用,则此时的常量就不是顶层常量了
auto k=ci,&l=i;//k是整数,1是整型引用
auto &m=ci,*p=&ci;//m是对整型常量的引用,p是指向整型常量的指针
//错误:i的类型是int而&ci的类型是const int
auto n=i,*p2=&ci;
-
decltype
-
如果我们仅仅是想根据初始值确定一个变量合适的数据类型,那么auto是最佳人选。而只有当我们需要推断某个表达式的数据类型,并将其作为一种新的数据类型重复使用(比如,定义多个相同类型变量)或者单独使用(比如,作为函数的返回值类型)时,我们才真正需要用到decltype。
-
decltype(f())sum=x;//sum的类型就是函数f的返回类型
编译器并不实际调用函数f,而是使用当调用发生时f的返回值类型作为sum的类型。
-
如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内):
const int ci=0,&cj=ci;
decltype(ci)x=0;//x的类型是const int
decltype(cj)y=x;//y的类型是const int&,y绑定到变量x
decltype(cj)z;//错误:z是一个引用,必须初始化
因为cj是一个引用,decltype(cj)的结果就是引用类型,因此作为引用的z必须被初始化。需要指出的是,引用从来都作为其所指对象的同义词出现,只有用在decltype处是一个例外。
如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。
//decltype的结果可以是引用类型
int i=42,*p=&i,&r=i;
decltype(r+0)b;//正确:加法的结果是int,因此b是一个(未初始化的)int
decltype(*p)c;//错误:c是int&,必须初始化
因为r是一个引用,因此decltype(r)的结果是引用类型。如果想让结果类型是r所指的类型,可以把r作为表达式的一部分,如r+0,显然这个表达式的结果将是一个具体值而非一个引用。另一方面,如果表达式的内容是解引用操作,则decltype将得到引用类型。正如我们所熟悉的那样,解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype(*p)的结果类型就是int&,而非int。
-
//decltype的表达式如果是加上了括号的变量,结果将是引用
decltype((i))d;//错误:d是int&,必须初始化
decltype(i)e;//正确:e是一个(未初始化的)int
decltype((variable))(注意是双层括号)的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用
自定义数据结构
-
定义
-
struct Sales data{}accum,trans,*salesptr;
//与上一条语句等价,但可能更好一些
struct Sales data{};
Sales data accum,trans,*salesptr; 分号表示声明符(通常为空)的结束。一般来说,最好不要把对象的定义和类的定义放在一起。这么做无异于把两种不同实体的定义混在了一条语句里,一会儿定义类,一会儿又定义变量,显然这是一种不被建议的行为。
-
如果一个指针指向的是一个结构体类型的变量,与结构体变量使用“.”符号引出成员变量不同的是,如果是指向结构体的指针,则应该用“->”符号引出其成员变量。
// 定义一个结构体变量
Employee Zengmei;
// 定义一个指针,并将其指向这个结构体变量
Employee* pZengmei = &Zengmei;
// 用“->”运算符,引用这个结构体指针变量的成员变量
pZengmei->m_strName = "Zengmei";
pZengmei->m_nAge = 28;
-
编写头文件
-
类一般都不定义在函数体内。当在函数体外部定义类时,在各个指定的源文件中可能只有一处该类的定义。而且,如果要在不同文件中使用同一个类,类的定义就必须保持一致。为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样。例如,库类型string在名为string的头文件中定义。又如,我们应该把Sales_data类定义在名为Sales_data.h的头文件中。头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明。
-
当预处理器看到#include标记时就会用指定的头文件的内容代替#include。C++程序还会用到的一项预处理功能是头文件保护符(headerguard),头文件保护符依赖于预处理变量(参见2.3.2节,第48页)。预处理变量有两种状态:已定义和未定义。#define指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:#ifdef当且仅当变量已定义时为真,#ifndef当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到#endif指令为止。使用这些功能就能有效地防止重复包含的发生:
#ifndef SALES DATA H
#define SALES DATA H
#include
struct Sales data{
std::string bookNo;
unsigned units sold=0;
double revenue=0.0;
};
#endif
第一次包含Sales_data.h时,#ifndef的检查结果为真,预处理器将顺序执行后面的操作直至遇到#endif为止。此时,预处理变量SALES_DATA_H的值将变为已定义,而且Sales_data.h也会被拷贝到我们的程序中来。后面如果再一次包含Sales_data.h,则#ifndef的检查结果将为假,编译器将忽略#ifndef到#endif之间的部分。
第3章 字符串、向量和数组
命名空间的using声明
-
std::cin的意思就是要使用命名空间std中的名字cin
-
using namespace::name;
using std::cin;
cin>>i;//正确:cin和std::cin含义相同
-
位于头文件的代码一般来说不应该使用using声明。这是因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个using声明,那么每个使用了该头文件的文件就都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。
标准库类型string
-
使用string类型必须首先包含string头文件
-
初始化
string s1;//默认初始化,s1是一个空字符串
string s2=s1;//s2是s1的副本
string s3="hiya";//s3是该字符串字面值的副本
tring s4(10,'c');//s4的内容是cccccccccc
string s1默认初始化,s1是一个空串
string s2(s1)s2是s1的副本
string s2=s1等价于s2(s1),s2是s1的副本
string s3("value")s3是字面值"value”的副本,除了字面值最后的那个空字符外
string s3="value”等价于s3("value"),s3是字面值“value”的副本
string s4(n,'c')把s4初始化为由连续n个字符c组成的串
-
直接初始化和拷贝初始化
-
当初始值只有一个时,使用直接初始化或拷贝初始化都行。如果像上面的s4那样初始化要用到的值有多个,一般来说只能使用直接初始化的方式
string s5="hiya";//拷贝初始化
string s6("hiya");//直接初始化
string s7(10,'c');//直接初始化,s7的内容是cccccccccc
-
对于用多个值进行初始化的情况,非要用拷贝初始化的方式来处理也不是不可以,不过需要显式地创建一个(临时)对象用于拷贝
string s8=string(10,'c');//拷贝初始化,s8的内容是cccccccccc
从命令行运行编译器
-
$cc prog1.cc【$是系统提示符,cc是编程名字,windows下生成是prog1.exe;UNIX下生成的是a.out】
-
$ prog1【windows下省略.exe,UNIX下不可忽略如$a.out】
-
查看main返回值【win下$ echo %ERRORLEVEL%,UNIX下$ echo $】
运行GNU
-
$ g++ -0 prog1 prog1.cc【$是系统提示符。-o prog1是编译器参数,指定了可执行文件的文件名。在不同的操作系统中,此命令生成一个名为prog1或prog1.exe的可执行文件。在UNIX系统中,可执行文件没有后缀;在Windows系统中,后缀为.exe。如果省略了-o prog1参数,在UNIX系统中编译器会生成一个名为a.out的可执行文件,在Windows系统中则会生成一个名为a.exe的可执行文件】
运行VS
-
C:UsersmePrograms> cl /EHsc prog1.cpp【C:UsersmePrograms>是系统提示符,UsersmePrograms是当前目录名(即当前文件夹)。命令cl调用编译器,/EHsc是编译器选项,用来打开标准异常处理。微软编译器会自动生成一个可执行文件,其名字与第一个源文件名对应。可执行文件的文件名与源文件名相同,后缀为.exe。在此例中,可执行文件的文件名为prog1.exe】
-
IO对象
-
cin、cout
-
cerr【标准错误。默认情况下,写到cerr的数据是不缓冲的。cerr通常用于输出错误信息或其他不属于程序正常逻辑的输出内容。】
-
clog【运行时的一般信息。默认情况下,写到clog的数据是被缓冲的。clog通常用于报告程序的执行信息,存入一个日志文件中。】
-
endl【写入endl的效果是结束当前行,并将与设备关联的缓冲区(buffer)中的内容刷到设备中。缓冲刷新操作可以保证到目前为止程序所产生的所有输出都真正写入输出流中,而不是仅停留在内存中等待写入流,序员常常在调试时添加打印语句。这类语句应该保证“一直”刷新流。否则,如果程序崩溃,输出可能还留在缓冲区中,从而导致关于程序崩溃位置的错误推断
-
注释
-
单行注释//这里是单行注释
-
多行注释
-
注释不能嵌套
*注释对不能嵌套。
*“不能嵌套”几个字会被认为是源码,```
控制流
-
控制流
-
读取数量不定的输入数据
#include
int main()
{
int sum=0,value=0;
//读取数据直到遇到文件尾,计算所有读入的值的和while(std::cin>>value)
sum+=value;//等价于sum=sum +value std::cout <<"Sum is:"<< sum<< std::endl;return 0;
}
此表达式从标准输入读取下一个数,保存在value中。输入运算符(参见1.2节,第7页)返回其左侧运算对象,在本例中是std::cin。因此,此循环条件实际上检测的是std::cin。
当我们使用一个istream对象作为条件时,其效果是检测流的状态。如果流是有效的,即流未遇到错误,那么检测成功。当遇到文件结束符(end-of-file),或遇到一个无效输入时(例如读入的值不是一个整数),istream对象的状态会变为无效。处于无效状态的istream对象会使条件变为假。
-
当从键盘向程序输入数据时,对于如何指出文件结束,不同操作系统有不同的约定。在Windows系统中,输入文件结束符的方法是敲Ctrl+Z(按住Ctrl键的同时按Z键),然后按Enter或Return键。在UNIX系统中,包括Mac OS X系统中,文件结束符输入是用Ctrl+D。
-
编译错误:语法错误、类型错误、声明错误
-
缓冲区【默认情况下,读cin会刷新cout;程序非正常终止时也会刷新cout。】
第Ⅰ部分 C++基础
第2章 变量和基本类型
基本内置类型
-
常用数据类型
常用数据类型 占用字节数 取值范围 short(前面可加unsigned)【整型】 2 -32768~32767(0~65535) int(前面可加unsigned)【整型】 4 -2147483648~2147483647(同理) long(前面可加unsigned)【整型】 4 -2147483648~2147483647(同理) long long(前面可加unsigned)【整型】 8 -9223372036854775808~…807(同理) float【实型】 4 负:-3.4×10^38~-3.4×10^-38 正:3.4×10^-38~3.4×10^38 double【实型】 8 负:-1.7×10^308~-1.7×10^-308 正:1.7×10^-308~1.7×10^308 long double【实型】 10 负:-1.1×10^4932~-1.1×10^-4932 正:1.1×10^-4932~1.1×10^4932 char(前面可加unsigned)【字符型】 1 -128~127(0~255) bool【布尔型】 1 true,false
-
类型转换
-
布尔类型:当我们把一个非布尔类型的算术值赋给布尔类型时,初始值为0则结果为false,否则结果为true。· 当我们把一个布尔值赋给非布尔类型时,初始值为false则结果为0,初始值为true则结果为1。
-
浮点数:当我们把一个浮点数赋给整数类型时,进行了近似处理。结果值将仅保留浮点数中小数点之前的部分。 当我们把一个整数值赋给浮点类型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失。
-
无符号:当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。例如,8比特大小的unsigned char可以表示0至255区间内的值,如果我们赋了一个区间以外的值,则实际的结果是该值对256取模后所得的余数。因此,把-1赋给8比特大小的unsigned char所得的结果是255。
-
字符:当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据。
-
当一个算术表达式中既有无符号数又有int值时,那个int值就会转换成无符号数
-
规则
-
隐式转换
高double⬅float
unsigned long int
long int
unsigned int
低int⬅char、short、bool
-
算术运算中转换:低到高
-
赋值运算中转换:
-
高向低:int a;a=15.5;
-
低向高:double a=10;cout<
-
显式转换:(数据类型)变量或常量、或数据类型(变量或常量或表达式)
-
常量
-
整型常量
-
二进制:0b10或者0B10
-
八进制:073
-
十进制:287
-
十八进制:0x1AF或者0x1AF
-
注意:cout << 1'100'000;分隔符不影响数值
-
实型常量
-
浮点表示法:-0.126
-
科学计数法:12.6E-5、126e-6→格式化:1.26E-4
-
字符常量
-
普通字符:‘A’(ASCII为65)
-
转义字符:
转义字符 功能 转义字符 功能 a 响铃 v 垂直制表 b 退格 ' f 换页 '' n 换行 \ r 回车(本行开头) nnn 1~3位八进制代表代表字符 t 水平制表,tab位置 xnn 十六进制代表队字符
注意:如果反斜线后面跟着的八进制数字超过3个,只有前3个数字与构成转义序列。例如,"1234"表示2个字符,即八进制数123对应的字符以及字符4。相反,x要用到后面跟着的所有数字,例如,"x1234"表示一个16位的字符,该字符由这4个十六进制数所对应的比特唯一确定。因为大多数机器的char型数据占8位,所以上面这个例子可能会报错。
-
字符串常量:
-
”Motnth“,长度为5,系统自动后面加‘ ’
-
string s = R"(zeng\mei)"; // 使用R"()"表示的原生字符串
-
指定字面值的类型
L’a'//宽字符型字面值,类型是wchar_t
u8"hi!"//utf-8字符串字面值(utf-8用8位编码一个Unicode字符)
42ULL//无符号整型字面值,类型是unsigned long 1ong
1E-3F//单精度浮点型字面值,类型是float
3.14159L//扩展精度浮点型字面值,类型是long double
变量
-
列表初始化
-
int units sold=0; int units sold={0}; int units sold{0};int units sold(0);都可以初始化和赋新值
-
当用于内置类型的变量时,这种初始化形式有一个重要特点:如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错:
long double 1d=3.1415926536;
int a{ld},b={1d};//错误:转换未执行,因为存在丢失信息的危险
intc(1d),d=1d;//正确:转换执行,且确实丢失了部分值 -
默认初始值
-
如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。定义于任何函数体之外的变量被初始化为0。
-
定义于函数体内的内置类型的对象如果没有初始化,则其值未定义。类的对象如果没有显式地初始化,则其值由类确定。
-
如果试图拷贝或以其他形式访问此类值将引发错误
-
变量声明和定义
-
如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显式地初始化变量:
extern int i;//声明i而非定义i
int j;//声明并定义
-
extern语句如果包含初始值就不再是声明,而变成定义了:
extern double pi=3.1416;//定义
-
变量能且只能被定义一次,但是可以被多次声明。所以,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。
-
标识符
-
用户自定义的标识符中不能连续出现两个下画线,也不能以下画线紧连大写字母开头。此外,定义在函数体外的标识符不能以下画线开头。
-
变量命名规范:
-
变量名一般用小写字母,如index,不要使用Index或INDEX
-
用户自定义的类名一般以大写字母开头,如Sales_item
-
如果标识符由多个单词组成,则单词间应有明显区分,如student_loan或studentLoan,不要使用studentloan
-
如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量
复合类型
-
引用(左值引用)
-
引用即别名:一般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值绑定(bind)在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。
int ival=1024;
int &refVal=ival;//refVal指向ival(是ival的另一个名字)
int &refVal2;//报错:引用必须被初始化
-
定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的:
refVal=2;//把2赋给refVal指向的对象,此处即是赋给了ival
int ii=refVal;//与ii=ival执行结果一样
-
因为引用本身不是一个对象,所以不能定义引用的引用
-
允许在一条语句中定义多个引用,其中每个引用标识符都必须以符号&开头:
int i=1024,i2=2048;//i和i2都是int
int &r=i,r2=i2;//r是一个引用,与i绑定在一起,r2是int
int i3=1024,&ri=i3;//i3是int,ri是一个引用,与i3绑定在一起
int &r3=i3,&r4=i2;//r3和r4都是引用
-
指针
-
指针与引用相比有很多不同点。其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。其二,指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
-
如果在一条语句中定义了几个指针变量,每个变量前面都必须有符号*
int*ipl,*ip2;//ipl和ip2都是指向int型对象的指针
double dp,*dp2;//dp2是指向double型对象的指针,dp是double型对象
-
因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。
-
空指针:
-
空指针形成方法:
int*pl=nullptr;//等价于int*pl=0;
int*p2=0;//直接将p2初始化为字面常量0
//需要首先#include cstdlib
int*p3=NULL;//等价于int*p3=0;
-
建议:初始化所有指针,如果实在不清楚指针应该指向何处,就把它初始化为nullptr或者0,这样程序就能检测并知道它没有指向任何具体的对象了
-
void *指针:是一种特殊的指针类型,可用于存放任意对象的地址。利用void*指针能做的事儿比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void*指针。不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。概括说来,以void*的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象。
-
理解复合类型的声明
-
一条定义语句可能定义出不同类型的变量:
//i是一个int型的数,p是一个int型指针,r是一个int型引用
int i=1024,*p=&i,&r=i;
-
int* p;//合法但是容易产生误导
我们说这种写法可能产生误导是因为int*放在一起好像是这条语句中所有变量共同的类型一样。其实恰恰相反,基本数据类型是int而非int*。*仅仅是修饰了p而已,对该声明语句中的其他变量,它并不产生任何作用:
int* pl,p2;//p1是指向int的指针,p2是int
-
指向指针的指针
int ival=1024;
int *pi=&ival;//pi指向一个int型的数
int **ppi=&pi;//ppi指向一个int型的指针
-
引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用:
int i=42;
int*p;//p是一个int型指针
int *&r=p;//r是一个对指针p的引用
r=&i;//r引用了一个指针,因此给r赋值&i就是令p指向i
*r=0;//解引用r得到i,也就是p指向的对象,将i的值改为0
要理解r的类型到底是什么,最简单的办法是从右向左阅读r的定义。离变量名最近的符号(此例中是&r的符号&)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针。最后,声明的基本数据类型部分指出r引用的是一个int指针
const限定符
-
初始化
-
因为const对象一旦创建后其值就不能再改变,所以const对象必须初始化
const inti=get size();//正确:运行时初始化
const intj=42;//正确:编译时初始化
const int k;//错误:k是一个未经初始化的常量
-
同时避免对同一变量的重复定义,默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量
-
如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字:
//file1.cc定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufsize=fcn();
//file1.h头文件
extern const int bufsize;//与file 1.cc中定义的bufsize是同一个
-
const的引用
-
const int ci=1024;
const int &rl=ci;//正确:引用及其对应的对象都是常量
r1=42;//错误:r1是对常量的引用
int &r2=ci;//错误:试图让一个非常量引用指向一个常量对象
-
C++程序员们经常把词组“对const的引用”简称为“常量引用”,这一简称还是挺靠谱的,不过前提是你得时刻记得这就是个简称而已。严格来说,并不存在常量引用。因为引用不是一个对象,所以我们没法让引用本身恒定不变。事实上,由于C++语言并不允许随意改变引用所绑定的对象,所以从这层意义上理解所有的引用又都算是常量。
-
引用的类型必须与其所引用对象的类型一致,但是有两个例外。第一种例外情况就是在初始化常量引用时允许用任意表达式作为初始值(临时常量=i;常量引用=;临时常量)
int i=42;
const int&rl=i;//允许将const ints绑定到一个普通int对象上
const int&r2=42;//正确:r1是一个常量引用
const int&r3=r1*2;//正确:r3是一个常量引用
int &r4=r1*2;//错误:r4是一个普通的非常量引用
r2绑定(非常量)整数i是合法的行为。然而,不允许通过r2修改i的值。尽管如此,i的值仍然允许通过其他途径修改,既可以直接给i赋值,也可以通过绑定到i的其他引用来修改
-
const double pi=3.14;//pi是个常量,它的值不能改变
double *ptr=&pi;//错误:ptr是一个普通指针
const double*cptr=&pi;//正确:cptr可以指向一个双精度常量
*Cptr=42;//错误:不能给*cptr赋值
-
指针的类型必须与其所指对象的类型一致,但是有两个例外。第一种例外情况是允许令一个指向常量的指针指向一个非常量对象。和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变
-
指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定为常量。常量指针(const pointer)必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。把*放在const关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味,即不变的是指针本身的值而非指向的那个值:
int errNumb=0;
int *const curErr=&errNumb;//curErr将一直指向errNumb
const double pi=3.14159;
const double *const pip=&pi;//pip是一个指向常量对象的常量指针
可以通过curErr修改errNumb值,不可以通过pip改变pi
-
顶层const和底层const
-
用名词顶层const(top-level const)表示指针本身是个常量,而用名词底层const(low-level const)表示指针所指的对象是一个常量
inti=0;
int *const pl=&i;//不能改变p1的值,这是一个顶层const
const int ci=42;//不能改变ci的值,这是一个顶层const
const int*p2=&ci;//允许改变p2的值,这是一个底层const
const int*const p3=p2;//靠右的 const 是顶层const,靠左的是底层const
const int&r=ci;//用于声明引用的 const 都是底层const
-
当执行对象的拷贝操作时,常量是顶层const还是底层const区别明显。其中,顶层const不受什么影响:
i=ci;//正确:拷贝ci的值,ci是一个顶层const,对此操作无影响
p2=p3;//正确:p2和p3指向的对象类型相同,p3顶层const的部分不影响
-
底层const的限制却不能忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之则不行:
int *p=p3;//错误:p3包含底层const的定义,而p没有
p2=p3;//正确:p2和p3都是底层const
p2=&i;//正确:int*能转换成const int*
int &r=ci;//错误:普通的int&不能绑定到int常量上
const int&r2=i;//正确:const int&可以绑定到一个普通int上
p3既是顶层const也是底层const,拷贝p3时可以不在乎它是一个顶层const,但是必须清楚它指向的对象得是一个常量。因此,不能用p3去初始化p,因为p指向的是一个普通的(非常量)整数。另一方面,p3的值可以赋给p2,是因为这两个指针都是底层const,尽管p3同时也是一个常量指针(顶层const),仅就这次赋值而言不会有什么影响
-
常量表达式
-
常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式
const int max files=20;//max files是常量表达式
const int limit=max files+1;//limit是常量表达式
int staff_size=27;//staff_size不是常量表达式
const int sz=get_size();//sz不是常量表达式
尽管sz本身是一个常量,但它的具体值直到运行时才能获取到,所以不是常量表达式
-
C++11 标准中,定义变量时可以用 constexpr 修饰,从而使该变量获得在编译阶段即可计算出结果的能力。常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为“字面值类型”。算术类型、引用和指针都属于字面值类型。自定义类Sales_item、IO库、string类型则不属于字面值类型,也就不能被定义成constexpr。尽管指针和引用都能定义成constexpr,但它们的初始值却受到严格限制。一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象。(函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。同样是在6.1.1节(第185页)中还将提到,允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址。因此,constexpr引用能绑定到这样的变量上,constexpr指针也能指向这样的变量)新标准允许定义一种特殊的constexpr函数。这种函数应该足够简单以使得编译时就可以计算其结果,这样就能用constexpr函数去初始化constexpr变量了。
处理类型
-
类型别名
-
方法一:
typedef double wages;//wages是double的同义词
typedef wages base,*p;//base是double的同义词,p是double*的同义词
-
方法二:
using SI=Sales item;//SI是Sales item的同义词
-
typedef char *pstring;
const pstring cstr=0;//cstr是指向char的常量指针
const pstring *ps;//ps是一个指针,它的对象是指向char的常量指针
char*别名是pstring
const char *cstr=0;//是对const pstring cstr的错误理解
声明语句中用到pstring时,其基本数据类型是指针。可是用char*重写了声明语句后,数据类型就变成了char,*成为了声明符的一部分。这样改写的结果是,const char成了基本数据类型。前后两种声明含义截然不同,前者声明了一个指向char的常量指针,改写后的形式则声明了一个指向const char的指针。
-
auto
-
C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。显然,auto定义的变量必须有初始值。
-
使用auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:
auto i=0,*p=&i;//正确:i是整数、p是整型指针
auto sz=0,pi=3.14;//错误:sz和pi的类型不一致
-
auto一般会忽略掉顶层const(参见2.4.3节,第57页),同时底层const则会保留下来,比如当初始值是一个指向常量的指针时:
const int ci=i,&cr=ci;
auto b=ci;//b是一个整数(ci的顶层const特性被忽略掉了)
auto c=cr;//c是一个整数(cr是ci的别名,ci本身是一个顶层const))
auto d=&i;//d是一个整型指针(整数的地址就是指向整数的指针)
auto e=&ci;//e是一个指向整数常量的指针(对常量对象取地址是一种底层const)
如果希望推断出的auto类型是一个顶层const,需要明确指出:
const auto f=ci;//ci的推演类型是int,f是const int
-
引用
auto &g=ci;//g是一个整型常量引用,绑定到ci
auto &h=42;//错误:不能为非常量引用绑定字面值
const auto &j=42;//正确:可以为常量引用绑定字面值
设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留。和往常一样,如果我们给初始值绑定一个引用,则此时的常量就不是顶层常量了
auto k=ci,&l=i;//k是整数,1是整型引用
auto &m=ci,*p=&ci;//m是对整型常量的引用,p是指向整型常量的指针
//错误:i的类型是int而&ci的类型是const int
auto n=i,*p2=&ci;
-
decltype
-
如果我们仅仅是想根据初始值确定一个变量合适的数据类型,那么auto是最佳人选。而只有当我们需要推断某个表达式的数据类型,并将其作为一种新的数据类型重复使用(比如,定义多个相同类型变量)或者单独使用(比如,作为函数的返回值类型)时,我们才真正需要用到decltype。
-
decltype(f())sum=x;//sum的类型就是函数f的返回类型
编译器并不实际调用函数f,而是使用当调用发生时f的返回值类型作为sum的类型。
-
如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内):
const int ci=0,&cj=ci;
decltype(ci)x=0;//x的类型是const int
decltype(cj)y=x;//y的类型是const int&,y绑定到变量x
decltype(cj)z;//错误:z是一个引用,必须初始化
因为cj是一个引用,decltype(cj)的结果就是引用类型,因此作为引用的z必须被初始化。需要指出的是,引用从来都作为其所指对象的同义词出现,只有用在decltype处是一个例外。
如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。
//decltype的结果可以是引用类型
int i=42,*p=&i,&r=i;
decltype(r+0)b;//正确:加法的结果是int,因此b是一个(未初始化的)int
decltype(*p)c;//错误:c是int&,必须初始化
因为r是一个引用,因此decltype(r)的结果是引用类型。如果想让结果类型是r所指的类型,可以把r作为表达式的一部分,如r+0,显然这个表达式的结果将是一个具体值而非一个引用。另一方面,如果表达式的内容是解引用操作,则decltype将得到引用类型。正如我们所熟悉的那样,解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype(*p)的结果类型就是int&,而非int。
-
//decltype的表达式如果是加上了括号的变量,结果将是引用
decltype((i))d;//错误:d是int&,必须初始化
decltype(i)e;//正确:e是一个(未初始化的)int
decltype((variable))(注意是双层括号)的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用
自定义数据结构
-
定义
-
struct Sales data{}accum,trans,*salesptr;
//与上一条语句等价,但可能更好一些
struct Sales data{};
Sales data accum,trans,*salesptr; 分号表示声明符(通常为空)的结束。一般来说,最好不要把对象的定义和类的定义放在一起。这么做无异于把两种不同实体的定义混在了一条语句里,一会儿定义类,一会儿又定义变量,显然这是一种不被建议的行为。
-
如果一个指针指向的是一个结构体类型的变量,与结构体变量使用“.”符号引出成员变量不同的是,如果是指向结构体的指针,则应该用“->”符号引出其成员变量。
// 定义一个结构体变量
Employee Zengmei;
// 定义一个指针,并将其指向这个结构体变量
Employee* pZengmei = &Zengmei;
// 用“->”运算符,引用这个结构体指针变量的成员变量
pZengmei->m_strName = "Zengmei";
pZengmei->m_nAge = 28;
-
编写头文件
-
类一般都不定义在函数体内。当在函数体外部定义类时,在各个指定的源文件中可能只有一处该类的定义。而且,如果要在不同文件中使用同一个类,类的定义就必须保持一致。为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样。例如,库类型string在名为string的头文件中定义。又如,我们应该把Sales_data类定义在名为Sales_data.h的头文件中。头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明。
-
当预处理器看到#include标记时就会用指定的头文件的内容代替#include。C++程序还会用到的一项预处理功能是头文件保护符(headerguard),头文件保护符依赖于预处理变量(参见2.3.2节,第48页)。预处理变量有两种状态:已定义和未定义。#define指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:#ifdef当且仅当变量已定义时为真,#ifndef当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到#endif指令为止。使用这些功能就能有效地防止重复包含的发生:
#ifndef SALES DATA H
#define SALES DATA H
#include
struct Sales data{
std::string bookNo;
unsigned units sold=0;
double revenue=0.0;
};
#endif
第一次包含Sales_data.h时,#ifndef的检查结果为真,预处理器将顺序执行后面的操作直至遇到#endif为止。此时,预处理变量SALES_DATA_H的值将变为已定义,而且Sales_data.h也会被拷贝到我们的程序中来。后面如果再一次包含Sales_data.h,则#ifndef的检查结果将为假,编译器将忽略#ifndef到#endif之间的部分。
第3章 字符串、向量和数组
命名空间的using声明
-
std::cin的意思就是要使用命名空间std中的名字cin
-
using namespace::name;
using std::cin;
cin>>i;//正确:cin和std::cin含义相同
-
位于头文件的代码一般来说不应该使用using声明。这是因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个using声明,那么每个使用了该头文件的文件就都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。
标准库类型string
-
使用string类型必须首先包含string头文件
-
初始化
string s1;//默认初始化,s1是一个空字符串
string s2=s1;//s2是s1的副本
string s3="hiya";//s3是该字符串字面值的副本
tring s4(10,'c');//s4的内容是cccccccccc
string s1默认初始化,s1是一个空串
string s2(s1)s2是s1的副本
string s2=s1等价于s2(s1),s2是s1的副本
string s3("value")s3是字面值"value”的副本,除了字面值最后的那个空字符外
string s3="value”等价于s3("value"),s3是字面值“value”的副本
string s4(n,'c')把s4初始化为由连续n个字符c组成的串
-
直接初始化和拷贝初始化
-
当初始值只有一个时,使用直接初始化或拷贝初始化都行。如果像上面的s4那样初始化要用到的值有多个,一般来说只能使用直接初始化的方式
string s5="hiya";//拷贝初始化
string s6("hiya");//直接初始化
string s7(10,'c');//直接初始化,s7的内容是cccccccccc
-
对于用多个值进行初始化的情况,非要用拷贝初始化的方式来处理也不是不可以,不过需要显式地创建一个(临时)对象用于拷贝
string s8=string(10,'c');//拷贝初始化,s8的内容是cccccccccc
单行注释//这里是单行注释
多行注释
注释不能嵌套
*注释对不能嵌套。 *“不能嵌套”几个字会被认为是源码,```
-
控制流
-
读取数量不定的输入数据
#include
int main() { int sum=0,value=0; //读取数据直到遇到文件尾,计算所有读入的值的和while(std::cin>>value) sum+=value;//等价于sum=sum +value std::cout <<"Sum is:"<< sum<< std::endl;return 0; } 此表达式从标准输入读取下一个数,保存在value中。输入运算符(参见1.2节,第7页)返回其左侧运算对象,在本例中是std::cin。因此,此循环条件实际上检测的是std::cin。
当我们使用一个istream对象作为条件时,其效果是检测流的状态。如果流是有效的,即流未遇到错误,那么检测成功。当遇到文件结束符(end-of-file),或遇到一个无效输入时(例如读入的值不是一个整数),istream对象的状态会变为无效。处于无效状态的istream对象会使条件变为假。
-
当从键盘向程序输入数据时,对于如何指出文件结束,不同操作系统有不同的约定。在Windows系统中,输入文件结束符的方法是敲Ctrl+Z(按住Ctrl键的同时按Z键),然后按Enter或Return键。在UNIX系统中,包括Mac OS X系统中,文件结束符输入是用Ctrl+D。
-
编译错误:语法错误、类型错误、声明错误
-
缓冲区【默认情况下,读cin会刷新cout;程序非正常终止时也会刷新cout。】
-
-
第Ⅰ部分 C++基础
第2章 变量和基本类型
基本内置类型
-
常用数据类型
常用数据类型 占用字节数 取值范围 short(前面可加unsigned)【整型】 2 -32768~32767(0~65535) int(前面可加unsigned)【整型】 4 -2147483648~2147483647(同理) long(前面可加unsigned)【整型】 4 -2147483648~2147483647(同理) long long(前面可加unsigned)【整型】 8 -9223372036854775808~…807(同理) float【实型】 4 负:-3.4×10^38~-3.4×10^-38 正:3.4×10^-38~3.4×10^38 double【实型】 8 负:-1.7×10^308~-1.7×10^-308 正:1.7×10^-308~1.7×10^308 long double【实型】 10 负:-1.1×10^4932~-1.1×10^-4932 正:1.1×10^-4932~1.1×10^4932 char(前面可加unsigned)【字符型】 1 -128~127(0~255) bool【布尔型】 1 true,false
-
类型转换
-
布尔类型:当我们把一个非布尔类型的算术值赋给布尔类型时,初始值为0则结果为false,否则结果为true。· 当我们把一个布尔值赋给非布尔类型时,初始值为false则结果为0,初始值为true则结果为1。
-
浮点数:当我们把一个浮点数赋给整数类型时,进行了近似处理。结果值将仅保留浮点数中小数点之前的部分。 当我们把一个整数值赋给浮点类型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失。
-
无符号:当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。例如,8比特大小的unsigned char可以表示0至255区间内的值,如果我们赋了一个区间以外的值,则实际的结果是该值对256取模后所得的余数。因此,把-1赋给8比特大小的unsigned char所得的结果是255。
-
字符:当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据。
-
当一个算术表达式中既有无符号数又有int值时,那个int值就会转换成无符号数
-
规则
-
隐式转换
高double⬅float
unsigned long int
long int
unsigned int
低int⬅char、short、bool
-
算术运算中转换:低到高
-
赋值运算中转换:
-
高向低:int a;a=15.5;
-
低向高:double a=10;cout<
-
显式转换:(数据类型)变量或常量、或数据类型(变量或常量或表达式)
-
常量
-
整型常量
-
二进制:0b10或者0B10
-
八进制:073
-
十进制:287
-
十八进制:0x1AF或者0x1AF
-
注意:cout << 1'100'000;分隔符不影响数值
-
实型常量
-
浮点表示法:-0.126
-
科学计数法:12.6E-5、126e-6→格式化:1.26E-4
-
字符常量
-
普通字符:‘A’(ASCII为65)
-
转义字符:
转义字符 功能 转义字符 功能 a 响铃 v 垂直制表 b 退格 ' f 换页 '' n 换行 \ r 回车(本行开头) nnn 1~3位八进制代表代表字符 t 水平制表,tab位置 xnn 十六进制代表队字符
注意:如果反斜线后面跟着的八进制数字超过3个,只有前3个数字与构成转义序列。例如,"1234"表示2个字符,即八进制数123对应的字符以及字符4。相反,x要用到后面跟着的所有数字,例如,"x1234"表示一个16位的字符,该字符由这4个十六进制数所对应的比特唯一确定。因为大多数机器的char型数据占8位,所以上面这个例子可能会报错。
-
字符串常量:
-
”Motnth“,长度为5,系统自动后面加‘ ’
-
string s = R"(zeng\mei)"; // 使用R"()"表示的原生字符串
-
指定字面值的类型
L’a'//宽字符型字面值,类型是wchar_t
u8"hi!"//utf-8字符串字面值(utf-8用8位编码一个Unicode字符)
42ULL//无符号整型字面值,类型是unsigned long 1ong
1E-3F//单精度浮点型字面值,类型是float
3.14159L//扩展精度浮点型字面值,类型是long double
变量
-
列表初始化
-
int units sold=0; int units sold={0}; int units sold{0};int units sold(0);都可以初始化和赋新值
-
当用于内置类型的变量时,这种初始化形式有一个重要特点:如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错:
long double 1d=3.1415926536;
int a{ld},b={1d};//错误:转换未执行,因为存在丢失信息的危险
intc(1d),d=1d;//正确:转换执行,且确实丢失了部分值 -
默认初始值
-
如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。定义于任何函数体之外的变量被初始化为0。
-
定义于函数体内的内置类型的对象如果没有初始化,则其值未定义。类的对象如果没有显式地初始化,则其值由类确定。
-
如果试图拷贝或以其他形式访问此类值将引发错误
-
变量声明和定义
-
如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显式地初始化变量:
extern int i;//声明i而非定义i
int j;//声明并定义
-
extern语句如果包含初始值就不再是声明,而变成定义了:
extern double pi=3.1416;//定义
-
变量能且只能被定义一次,但是可以被多次声明。所以,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。
-
标识符
-
用户自定义的标识符中不能连续出现两个下画线,也不能以下画线紧连大写字母开头。此外,定义在函数体外的标识符不能以下画线开头。
-
变量命名规范:
-
变量名一般用小写字母,如index,不要使用Index或INDEX
-
用户自定义的类名一般以大写字母开头,如Sales_item
-
如果标识符由多个单词组成,则单词间应有明显区分,如student_loan或studentLoan,不要使用studentloan
-
如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量
复合类型
-
引用(左值引用)
-
引用即别名:一般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值绑定(bind)在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。
int ival=1024;
int &refVal=ival;//refVal指向ival(是ival的另一个名字)
int &refVal2;//报错:引用必须被初始化
-
定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的:
refVal=2;//把2赋给refVal指向的对象,此处即是赋给了ival
int ii=refVal;//与ii=ival执行结果一样
-
因为引用本身不是一个对象,所以不能定义引用的引用
-
允许在一条语句中定义多个引用,其中每个引用标识符都必须以符号&开头:
int i=1024,i2=2048;//i和i2都是int
int &r=i,r2=i2;//r是一个引用,与i绑定在一起,r2是int
int i3=1024,&ri=i3;//i3是int,ri是一个引用,与i3绑定在一起
int &r3=i3,&r4=i2;//r3和r4都是引用
-
指针
-
指针与引用相比有很多不同点。其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。其二,指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
-
如果在一条语句中定义了几个指针变量,每个变量前面都必须有符号*
int*ipl,*ip2;//ipl和ip2都是指向int型对象的指针
double dp,*dp2;//dp2是指向double型对象的指针,dp是double型对象
-
因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。
-
空指针:
-
空指针形成方法:
int*pl=nullptr;//等价于int*pl=0;
int*p2=0;//直接将p2初始化为字面常量0
//需要首先#include cstdlib
int*p3=NULL;//等价于int*p3=0;
-
建议:初始化所有指针,如果实在不清楚指针应该指向何处,就把它初始化为nullptr或者0,这样程序就能检测并知道它没有指向任何具体的对象了
-
void *指针:是一种特殊的指针类型,可用于存放任意对象的地址。利用void*指针能做的事儿比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void*指针。不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。概括说来,以void*的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象。
-
理解复合类型的声明
-
一条定义语句可能定义出不同类型的变量:
//i是一个int型的数,p是一个int型指针,r是一个int型引用
int i=1024,*p=&i,&r=i;
-
int* p;//合法但是容易产生误导
我们说这种写法可能产生误导是因为int*放在一起好像是这条语句中所有变量共同的类型一样。其实恰恰相反,基本数据类型是int而非int*。*仅仅是修饰了p而已,对该声明语句中的其他变量,它并不产生任何作用:
int* pl,p2;//p1是指向int的指针,p2是int
-
指向指针的指针
int ival=1024;
int *pi=&ival;//pi指向一个int型的数
int **ppi=&pi;//ppi指向一个int型的指针
-
引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用:
int i=42;
int*p;//p是一个int型指针
int *&r=p;//r是一个对指针p的引用
r=&i;//r引用了一个指针,因此给r赋值&i就是令p指向i
*r=0;//解引用r得到i,也就是p指向的对象,将i的值改为0
要理解r的类型到底是什么,最简单的办法是从右向左阅读r的定义。离变量名最近的符号(此例中是&r的符号&)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针。最后,声明的基本数据类型部分指出r引用的是一个int指针
const限定符
-
初始化
-
因为const对象一旦创建后其值就不能再改变,所以const对象必须初始化
const inti=get size();//正确:运行时初始化
const intj=42;//正确:编译时初始化
const int k;//错误:k是一个未经初始化的常量
-
同时避免对同一变量的重复定义,默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量
-
如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字:
//file1.cc定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufsize=fcn();
//file1.h头文件
extern const int bufsize;//与file 1.cc中定义的bufsize是同一个
-
const的引用
-
const int ci=1024;
const int &rl=ci;//正确:引用及其对应的对象都是常量
r1=42;//错误:r1是对常量的引用
int &r2=ci;//错误:试图让一个非常量引用指向一个常量对象
-
C++程序员们经常把词组“对const的引用”简称为“常量引用”,这一简称还是挺靠谱的,不过前提是你得时刻记得这就是个简称而已。严格来说,并不存在常量引用。因为引用不是一个对象,所以我们没法让引用本身恒定不变。事实上,由于C++语言并不允许随意改变引用所绑定的对象,所以从这层意义上理解所有的引用又都算是常量。
-
引用的类型必须与其所引用对象的类型一致,但是有两个例外。第一种例外情况就是在初始化常量引用时允许用任意表达式作为初始值(临时常量=i;常量引用=;临时常量)
int i=42;
const int&rl=i;//允许将const ints绑定到一个普通int对象上
const int&r2=42;//正确:r1是一个常量引用
const int&r3=r1*2;//正确:r3是一个常量引用
int &r4=r1*2;//错误:r4是一个普通的非常量引用
r2绑定(非常量)整数i是合法的行为。然而,不允许通过r2修改i的值。尽管如此,i的值仍然允许通过其他途径修改,既可以直接给i赋值,也可以通过绑定到i的其他引用来修改
-
const double pi=3.14;//pi是个常量,它的值不能改变
double *ptr=&pi;//错误:ptr是一个普通指针
const double*cptr=&pi;//正确:cptr可以指向一个双精度常量
*Cptr=42;//错误:不能给*cptr赋值
-
指针的类型必须与其所指对象的类型一致,但是有两个例外。第一种例外情况是允许令一个指向常量的指针指向一个非常量对象。和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变
-
指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定为常量。常量指针(const pointer)必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。把*放在const关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味,即不变的是指针本身的值而非指向的那个值:
int errNumb=0;
int *const curErr=&errNumb;//curErr将一直指向errNumb
const double pi=3.14159;
const double *const pip=&pi;//pip是一个指向常量对象的常量指针
可以通过curErr修改errNumb值,不可以通过pip改变pi
-
顶层const和底层const
-
用名词顶层const(top-level const)表示指针本身是个常量,而用名词底层const(low-level const)表示指针所指的对象是一个常量
inti=0;
int *const pl=&i;//不能改变p1的值,这是一个顶层const
const int ci=42;//不能改变ci的值,这是一个顶层const
const int*p2=&ci;//允许改变p2的值,这是一个底层const
const int*const p3=p2;//靠右的 const 是顶层const,靠左的是底层const
const int&r=ci;//用于声明引用的 const 都是底层const
-
当执行对象的拷贝操作时,常量是顶层const还是底层const区别明显。其中,顶层const不受什么影响:
i=ci;//正确:拷贝ci的值,ci是一个顶层const,对此操作无影响
p2=p3;//正确:p2和p3指向的对象类型相同,p3顶层const的部分不影响
-
底层const的限制却不能忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之则不行:
int *p=p3;//错误:p3包含底层const的定义,而p没有
p2=p3;//正确:p2和p3都是底层const
p2=&i;//正确:int*能转换成const int*
int &r=ci;//错误:普通的int&不能绑定到int常量上
const int&r2=i;//正确:const int&可以绑定到一个普通int上
p3既是顶层const也是底层const,拷贝p3时可以不在乎它是一个顶层const,但是必须清楚它指向的对象得是一个常量。因此,不能用p3去初始化p,因为p指向的是一个普通的(非常量)整数。另一方面,p3的值可以赋给p2,是因为这两个指针都是底层const,尽管p3同时也是一个常量指针(顶层const),仅就这次赋值而言不会有什么影响
-
常量表达式
-
常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式
const int max files=20;//max files是常量表达式
const int limit=max files+1;//limit是常量表达式
int staff_size=27;//staff_size不是常量表达式
const int sz=get_size();//sz不是常量表达式
尽管sz本身是一个常量,但它的具体值直到运行时才能获取到,所以不是常量表达式
-
C++11 标准中,定义变量时可以用 constexpr 修饰,从而使该变量获得在编译阶段即可计算出结果的能力。常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为“字面值类型”。算术类型、引用和指针都属于字面值类型。自定义类Sales_item、IO库、string类型则不属于字面值类型,也就不能被定义成constexpr。尽管指针和引用都能定义成constexpr,但它们的初始值却受到严格限制。一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象。(函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。同样是在6.1.1节(第185页)中还将提到,允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址。因此,constexpr引用能绑定到这样的变量上,constexpr指针也能指向这样的变量)新标准允许定义一种特殊的constexpr函数。这种函数应该足够简单以使得编译时就可以计算其结果,这样就能用constexpr函数去初始化constexpr变量了。
处理类型
-
类型别名
-
方法一:
typedef double wages;//wages是double的同义词
typedef wages base,*p;//base是double的同义词,p是double*的同义词
-
方法二:
using SI=Sales item;//SI是Sales item的同义词
-
typedef char *pstring;
const pstring cstr=0;//cstr是指向char的常量指针
const pstring *ps;//ps是一个指针,它的对象是指向char的常量指针
char*别名是pstring
const char *cstr=0;//是对const pstring cstr的错误理解
声明语句中用到pstring时,其基本数据类型是指针。可是用char*重写了声明语句后,数据类型就变成了char,*成为了声明符的一部分。这样改写的结果是,const char成了基本数据类型。前后两种声明含义截然不同,前者声明了一个指向char的常量指针,改写后的形式则声明了一个指向const char的指针。
-
auto
-
C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。显然,auto定义的变量必须有初始值。
-
使用auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:
auto i=0,*p=&i;//正确:i是整数、p是整型指针
auto sz=0,pi=3.14;//错误:sz和pi的类型不一致
-
auto一般会忽略掉顶层const(参见2.4.3节,第57页),同时底层const则会保留下来,比如当初始值是一个指向常量的指针时:
const int ci=i,&cr=ci;
auto b=ci;//b是一个整数(ci的顶层const特性被忽略掉了)
auto c=cr;//c是一个整数(cr是ci的别名,ci本身是一个顶层const))
auto d=&i;//d是一个整型指针(整数的地址就是指向整数的指针)
auto e=&ci;//e是一个指向整数常量的指针(对常量对象取地址是一种底层const)
如果希望推断出的auto类型是一个顶层const,需要明确指出:
const auto f=ci;//ci的推演类型是int,f是const int
-
引用
auto &g=ci;//g是一个整型常量引用,绑定到ci
auto &h=42;//错误:不能为非常量引用绑定字面值
const auto &j=42;//正确:可以为常量引用绑定字面值
设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留。和往常一样,如果我们给初始值绑定一个引用,则此时的常量就不是顶层常量了
auto k=ci,&l=i;//k是整数,1是整型引用
auto &m=ci,*p=&ci;//m是对整型常量的引用,p是指向整型常量的指针
//错误:i的类型是int而&ci的类型是const int
auto n=i,*p2=&ci;
-
decltype
-
如果我们仅仅是想根据初始值确定一个变量合适的数据类型,那么auto是最佳人选。而只有当我们需要推断某个表达式的数据类型,并将其作为一种新的数据类型重复使用(比如,定义多个相同类型变量)或者单独使用(比如,作为函数的返回值类型)时,我们才真正需要用到decltype。
-
decltype(f())sum=x;//sum的类型就是函数f的返回类型
编译器并不实际调用函数f,而是使用当调用发生时f的返回值类型作为sum的类型。
-
如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内):
const int ci=0,&cj=ci;
decltype(ci)x=0;//x的类型是const int
decltype(cj)y=x;//y的类型是const int&,y绑定到变量x
decltype(cj)z;//错误:z是一个引用,必须初始化
因为cj是一个引用,decltype(cj)的结果就是引用类型,因此作为引用的z必须被初始化。需要指出的是,引用从来都作为其所指对象的同义词出现,只有用在decltype处是一个例外。
如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。
//decltype的结果可以是引用类型
int i=42,*p=&i,&r=i;
decltype(r+0)b;//正确:加法的结果是int,因此b是一个(未初始化的)int
decltype(*p)c;//错误:c是int&,必须初始化
因为r是一个引用,因此decltype(r)的结果是引用类型。如果想让结果类型是r所指的类型,可以把r作为表达式的一部分,如r+0,显然这个表达式的结果将是一个具体值而非一个引用。另一方面,如果表达式的内容是解引用操作,则decltype将得到引用类型。正如我们所熟悉的那样,解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype(*p)的结果类型就是int&,而非int。
-
//decltype的表达式如果是加上了括号的变量,结果将是引用
decltype((i))d;//错误:d是int&,必须初始化
decltype(i)e;//正确:e是一个(未初始化的)int
decltype((variable))(注意是双层括号)的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用
自定义数据结构
-
定义
-
struct Sales data{}accum,trans,*salesptr;
//与上一条语句等价,但可能更好一些
struct Sales data{};
Sales data accum,trans,*salesptr; 分号表示声明符(通常为空)的结束。一般来说,最好不要把对象的定义和类的定义放在一起。这么做无异于把两种不同实体的定义混在了一条语句里,一会儿定义类,一会儿又定义变量,显然这是一种不被建议的行为。
-
如果一个指针指向的是一个结构体类型的变量,与结构体变量使用“.”符号引出成员变量不同的是,如果是指向结构体的指针,则应该用“->”符号引出其成员变量。
// 定义一个结构体变量
Employee Zengmei;
// 定义一个指针,并将其指向这个结构体变量
Employee* pZengmei = &Zengmei;
// 用“->”运算符,引用这个结构体指针变量的成员变量
pZengmei->m_strName = "Zengmei";
pZengmei->m_nAge = 28;
-
编写头文件
-
类一般都不定义在函数体内。当在函数体外部定义类时,在各个指定的源文件中可能只有一处该类的定义。而且,如果要在不同文件中使用同一个类,类的定义就必须保持一致。为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样。例如,库类型string在名为string的头文件中定义。又如,我们应该把Sales_data类定义在名为Sales_data.h的头文件中。头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明。
-
当预处理器看到#include标记时就会用指定的头文件的内容代替#include。C++程序还会用到的一项预处理功能是头文件保护符(headerguard),头文件保护符依赖于预处理变量(参见2.3.2节,第48页)。预处理变量有两种状态:已定义和未定义。#define指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:#ifdef当且仅当变量已定义时为真,#ifndef当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到#endif指令为止。使用这些功能就能有效地防止重复包含的发生:
#ifndef SALES DATA H
#define SALES DATA H
#include
struct Sales data{
std::string bookNo;
unsigned units sold=0;
double revenue=0.0;
};
#endif
第一次包含Sales_data.h时,#ifndef的检查结果为真,预处理器将顺序执行后面的操作直至遇到#endif为止。此时,预处理变量SALES_DATA_H的值将变为已定义,而且Sales_data.h也会被拷贝到我们的程序中来。后面如果再一次包含Sales_data.h,则#ifndef的检查结果将为假,编译器将忽略#ifndef到#endif之间的部分。
第3章 字符串、向量和数组
命名空间的using声明
-
std::cin的意思就是要使用命名空间std中的名字cin
-
using namespace::name;
using std::cin;
cin>>i;//正确:cin和std::cin含义相同
-
位于头文件的代码一般来说不应该使用using声明。这是因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个using声明,那么每个使用了该头文件的文件就都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。
标准库类型string
-
使用string类型必须首先包含string头文件
-
初始化
string s1;//默认初始化,s1是一个空字符串
string s2=s1;//s2是s1的副本
string s3="hiya";//s3是该字符串字面值的副本
tring s4(10,'c');//s4的内容是cccccccccc
string s1默认初始化,s1是一个空串
string s2(s1)s2是s1的副本
string s2=s1等价于s2(s1),s2是s1的副本
string s3("value")s3是字面值"value”的副本,除了字面值最后的那个空字符外
string s3="value”等价于s3("value"),s3是字面值“value”的副本
string s4(n,'c')把s4初始化为由连续n个字符c组成的串
-
直接初始化和拷贝初始化
-
当初始值只有一个时,使用直接初始化或拷贝初始化都行。如果像上面的s4那样初始化要用到的值有多个,一般来说只能使用直接初始化的方式
string s5="hiya";//拷贝初始化
string s6("hiya");//直接初始化
string s7(10,'c');//直接初始化,s7的内容是cccccccccc
-
对于用多个值进行初始化的情况,非要用拷贝初始化的方式来处理也不是不可以,不过需要显式地创建一个(临时)对象用于拷贝
string s8=string(10,'c');//拷贝初始化,s8的内容是cccccccccc
基本内置类型
-
常用数据类型
常用数据类型 占用字节数 取值范围 short(前面可加unsigned)【整型】 2 -32768~32767(0~65535) int(前面可加unsigned)【整型】 4 -2147483648~2147483647(同理) long(前面可加unsigned)【整型】 4 -2147483648~2147483647(同理) long long(前面可加unsigned)【整型】 8 -9223372036854775808~…807(同理) float【实型】 4 负:-3.4×10^38~-3.4×10^-38 正:3.4×10^-38~3.4×10^38 double【实型】 8 负:-1.7×10^308~-1.7×10^-308 正:1.7×10^-308~1.7×10^308 long double【实型】 10 负:-1.1×10^4932~-1.1×10^-4932 正:1.1×10^-4932~1.1×10^4932 char(前面可加unsigned)【字符型】 1 -128~127(0~255) bool【布尔型】 1 true,false
-
类型转换
-
布尔类型:当我们把一个非布尔类型的算术值赋给布尔类型时,初始值为0则结果为false,否则结果为true。· 当我们把一个布尔值赋给非布尔类型时,初始值为false则结果为0,初始值为true则结果为1。
-
浮点数:当我们把一个浮点数赋给整数类型时,进行了近似处理。结果值将仅保留浮点数中小数点之前的部分。 当我们把一个整数值赋给浮点类型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失。
-
无符号:当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。例如,8比特大小的unsigned char可以表示0至255区间内的值,如果我们赋了一个区间以外的值,则实际的结果是该值对256取模后所得的余数。因此,把-1赋给8比特大小的unsigned char所得的结果是255。
-
字符:当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据。
-
当一个算术表达式中既有无符号数又有int值时,那个int值就会转换成无符号数
-
规则
-
隐式转换
高double⬅float
unsigned long int
long int
unsigned int
低int⬅char、short、bool
-
算术运算中转换:低到高
-
赋值运算中转换:
-
高向低:int a;a=15.5;
-
低向高:double a=10;cout<
-
显式转换:(数据类型)变量或常量、或数据类型(变量或常量或表达式)
-
常量
-
整型常量
-
二进制:0b10或者0B10
-
八进制:073
-
十进制:287
-
十八进制:0x1AF或者0x1AF
-
注意:cout << 1'100'000;分隔符不影响数值
-
实型常量
-
浮点表示法:-0.126
-
科学计数法:12.6E-5、126e-6→格式化:1.26E-4
-
字符常量
-
普通字符:‘A’(ASCII为65)
-
转义字符:
转义字符 功能 转义字符 功能 a 响铃 v 垂直制表 b 退格 ' f 换页 '' n 换行 \ r 回车(本行开头) nnn 1~3位八进制代表代表字符 t 水平制表,tab位置 xnn 十六进制代表队字符
注意:如果反斜线后面跟着的八进制数字超过3个,只有前3个数字与构成转义序列。例如,"1234"表示2个字符,即八进制数123对应的字符以及字符4。相反,x要用到后面跟着的所有数字,例如,"x1234"表示一个16位的字符,该字符由这4个十六进制数所对应的比特唯一确定。因为大多数机器的char型数据占8位,所以上面这个例子可能会报错。
-
字符串常量:
-
”Motnth“,长度为5,系统自动后面加‘ ’
-
string s = R"(zeng\mei)"; // 使用R"()"表示的原生字符串
-
指定字面值的类型
L’a'//宽字符型字面值,类型是wchar_t
u8"hi!"//utf-8字符串字面值(utf-8用8位编码一个Unicode字符)
42ULL//无符号整型字面值,类型是unsigned long 1ong
1E-3F//单精度浮点型字面值,类型是float
3.14159L//扩展精度浮点型字面值,类型是long double
变量
-
列表初始化
-
int units sold=0; int units sold={0}; int units sold{0};int units sold(0);都可以初始化和赋新值
-
当用于内置类型的变量时,这种初始化形式有一个重要特点:如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错:
long double 1d=3.1415926536;
int a{ld},b={1d};//错误:转换未执行,因为存在丢失信息的危险
intc(1d),d=1d;//正确:转换执行,且确实丢失了部分值 -
默认初始值
-
如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。定义于任何函数体之外的变量被初始化为0。
-
定义于函数体内的内置类型的对象如果没有初始化,则其值未定义。类的对象如果没有显式地初始化,则其值由类确定。
-
如果试图拷贝或以其他形式访问此类值将引发错误
-
变量声明和定义
-
如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显式地初始化变量:
extern int i;//声明i而非定义i
int j;//声明并定义
-
extern语句如果包含初始值就不再是声明,而变成定义了:
extern double pi=3.1416;//定义
-
变量能且只能被定义一次,但是可以被多次声明。所以,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。
-
标识符
-
用户自定义的标识符中不能连续出现两个下画线,也不能以下画线紧连大写字母开头。此外,定义在函数体外的标识符不能以下画线开头。
-
变量命名规范:
-
变量名一般用小写字母,如index,不要使用Index或INDEX
-
用户自定义的类名一般以大写字母开头,如Sales_item
-
如果标识符由多个单词组成,则单词间应有明显区分,如student_loan或studentLoan,不要使用studentloan
-
如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量
复合类型
-
引用(左值引用)
-
引用即别名:一般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值绑定(bind)在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。
int ival=1024;
int &refVal=ival;//refVal指向ival(是ival的另一个名字)
int &refVal2;//报错:引用必须被初始化
-
定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的:
refVal=2;//把2赋给refVal指向的对象,此处即是赋给了ival
int ii=refVal;//与ii=ival执行结果一样
-
因为引用本身不是一个对象,所以不能定义引用的引用
-
允许在一条语句中定义多个引用,其中每个引用标识符都必须以符号&开头:
int i=1024,i2=2048;//i和i2都是int
int &r=i,r2=i2;//r是一个引用,与i绑定在一起,r2是int
int i3=1024,&ri=i3;//i3是int,ri是一个引用,与i3绑定在一起
int &r3=i3,&r4=i2;//r3和r4都是引用
-
指针
-
指针与引用相比有很多不同点。其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。其二,指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
-
如果在一条语句中定义了几个指针变量,每个变量前面都必须有符号*
int*ipl,*ip2;//ipl和ip2都是指向int型对象的指针
double dp,*dp2;//dp2是指向double型对象的指针,dp是double型对象
-
因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。
-
空指针:
-
空指针形成方法:
int*pl=nullptr;//等价于int*pl=0;
int*p2=0;//直接将p2初始化为字面常量0
//需要首先#include cstdlib
int*p3=NULL;//等价于int*p3=0;
-
建议:初始化所有指针,如果实在不清楚指针应该指向何处,就把它初始化为nullptr或者0,这样程序就能检测并知道它没有指向任何具体的对象了
-
void *指针:是一种特殊的指针类型,可用于存放任意对象的地址。利用void*指针能做的事儿比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void*指针。不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。概括说来,以void*的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象。
-
理解复合类型的声明
-
一条定义语句可能定义出不同类型的变量:
//i是一个int型的数,p是一个int型指针,r是一个int型引用
int i=1024,*p=&i,&r=i;
-
int* p;//合法但是容易产生误导
我们说这种写法可能产生误导是因为int*放在一起好像是这条语句中所有变量共同的类型一样。其实恰恰相反,基本数据类型是int而非int*。*仅仅是修饰了p而已,对该声明语句中的其他变量,它并不产生任何作用:
int* pl,p2;//p1是指向int的指针,p2是int
-
指向指针的指针
int ival=1024;
int *pi=&ival;//pi指向一个int型的数
int **ppi=&pi;//ppi指向一个int型的指针
-
引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用:
int i=42;
int*p;//p是一个int型指针
int *&r=p;//r是一个对指针p的引用
r=&i;//r引用了一个指针,因此给r赋值&i就是令p指向i
*r=0;//解引用r得到i,也就是p指向的对象,将i的值改为0
要理解r的类型到底是什么,最简单的办法是从右向左阅读r的定义。离变量名最近的符号(此例中是&r的符号&)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针。最后,声明的基本数据类型部分指出r引用的是一个int指针
const限定符
-
初始化
-
因为const对象一旦创建后其值就不能再改变,所以const对象必须初始化
const inti=get size();//正确:运行时初始化
const intj=42;//正确:编译时初始化
const int k;//错误:k是一个未经初始化的常量
-
同时避免对同一变量的重复定义,默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量
-
如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字:
//file1.cc定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufsize=fcn();
//file1.h头文件
extern const int bufsize;//与file 1.cc中定义的bufsize是同一个
-
const的引用
-
const int ci=1024;
const int &rl=ci;//正确:引用及其对应的对象都是常量
r1=42;//错误:r1是对常量的引用
int &r2=ci;//错误:试图让一个非常量引用指向一个常量对象
-
C++程序员们经常把词组“对const的引用”简称为“常量引用”,这一简称还是挺靠谱的,不过前提是你得时刻记得这就是个简称而已。严格来说,并不存在常量引用。因为引用不是一个对象,所以我们没法让引用本身恒定不变。事实上,由于C++语言并不允许随意改变引用所绑定的对象,所以从这层意义上理解所有的引用又都算是常量。
-
引用的类型必须与其所引用对象的类型一致,但是有两个例外。第一种例外情况就是在初始化常量引用时允许用任意表达式作为初始值(临时常量=i;常量引用=;临时常量)
int i=42;
const int&rl=i;//允许将const ints绑定到一个普通int对象上
const int&r2=42;//正确:r1是一个常量引用
const int&r3=r1*2;//正确:r3是一个常量引用
int &r4=r1*2;//错误:r4是一个普通的非常量引用
r2绑定(非常量)整数i是合法的行为。然而,不允许通过r2修改i的值。尽管如此,i的值仍然允许通过其他途径修改,既可以直接给i赋值,也可以通过绑定到i的其他引用来修改
-
const double pi=3.14;//pi是个常量,它的值不能改变
double *ptr=&pi;//错误:ptr是一个普通指针
const double*cptr=&pi;//正确:cptr可以指向一个双精度常量
*Cptr=42;//错误:不能给*cptr赋值
-
指针的类型必须与其所指对象的类型一致,但是有两个例外。第一种例外情况是允许令一个指向常量的指针指向一个非常量对象。和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变
-
指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定为常量。常量指针(const pointer)必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。把*放在const关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味,即不变的是指针本身的值而非指向的那个值:
int errNumb=0;
int *const curErr=&errNumb;//curErr将一直指向errNumb
const double pi=3.14159;
const double *const pip=&pi;//pip是一个指向常量对象的常量指针
可以通过curErr修改errNumb值,不可以通过pip改变pi
-
顶层const和底层const
-
用名词顶层const(top-level const)表示指针本身是个常量,而用名词底层const(low-level const)表示指针所指的对象是一个常量
inti=0;
int *const pl=&i;//不能改变p1的值,这是一个顶层const
const int ci=42;//不能改变ci的值,这是一个顶层const
const int*p2=&ci;//允许改变p2的值,这是一个底层const
const int*const p3=p2;//靠右的 const 是顶层const,靠左的是底层const
const int&r=ci;//用于声明引用的 const 都是底层const
-
当执行对象的拷贝操作时,常量是顶层const还是底层const区别明显。其中,顶层const不受什么影响:
i=ci;//正确:拷贝ci的值,ci是一个顶层const,对此操作无影响
p2=p3;//正确:p2和p3指向的对象类型相同,p3顶层const的部分不影响
-
底层const的限制却不能忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之则不行:
int *p=p3;//错误:p3包含底层const的定义,而p没有
p2=p3;//正确:p2和p3都是底层const
p2=&i;//正确:int*能转换成const int*
int &r=ci;//错误:普通的int&不能绑定到int常量上
const int&r2=i;//正确:const int&可以绑定到一个普通int上
p3既是顶层const也是底层const,拷贝p3时可以不在乎它是一个顶层const,但是必须清楚它指向的对象得是一个常量。因此,不能用p3去初始化p,因为p指向的是一个普通的(非常量)整数。另一方面,p3的值可以赋给p2,是因为这两个指针都是底层const,尽管p3同时也是一个常量指针(顶层const),仅就这次赋值而言不会有什么影响
-
常量表达式
-
常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式
const int max files=20;//max files是常量表达式
const int limit=max files+1;//limit是常量表达式
int staff_size=27;//staff_size不是常量表达式
const int sz=get_size();//sz不是常量表达式
尽管sz本身是一个常量,但它的具体值直到运行时才能获取到,所以不是常量表达式
-
C++11 标准中,定义变量时可以用 constexpr 修饰,从而使该变量获得在编译阶段即可计算出结果的能力。常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为“字面值类型”。算术类型、引用和指针都属于字面值类型。自定义类Sales_item、IO库、string类型则不属于字面值类型,也就不能被定义成constexpr。尽管指针和引用都能定义成constexpr,但它们的初始值却受到严格限制。一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象。(函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。同样是在6.1.1节(第185页)中还将提到,允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址。因此,constexpr引用能绑定到这样的变量上,constexpr指针也能指向这样的变量)新标准允许定义一种特殊的constexpr函数。这种函数应该足够简单以使得编译时就可以计算其结果,这样就能用constexpr函数去初始化constexpr变量了。
处理类型
-
类型别名
-
方法一:
typedef double wages;//wages是double的同义词
typedef wages base,*p;//base是double的同义词,p是double*的同义词
-
方法二:
using SI=Sales item;//SI是Sales item的同义词
-
typedef char *pstring;
const pstring cstr=0;//cstr是指向char的常量指针
const pstring *ps;//ps是一个指针,它的对象是指向char的常量指针
char*别名是pstring
const char *cstr=0;//是对const pstring cstr的错误理解
声明语句中用到pstring时,其基本数据类型是指针。可是用char*重写了声明语句后,数据类型就变成了char,*成为了声明符的一部分。这样改写的结果是,const char成了基本数据类型。前后两种声明含义截然不同,前者声明了一个指向char的常量指针,改写后的形式则声明了一个指向const char的指针。
-
auto
-
C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。显然,auto定义的变量必须有初始值。
-
使用auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:
auto i=0,*p=&i;//正确:i是整数、p是整型指针
auto sz=0,pi=3.14;//错误:sz和pi的类型不一致
-
auto一般会忽略掉顶层const(参见2.4.3节,第57页),同时底层const则会保留下来,比如当初始值是一个指向常量的指针时:
const int ci=i,&cr=ci;
auto b=ci;//b是一个整数(ci的顶层const特性被忽略掉了)
auto c=cr;//c是一个整数(cr是ci的别名,ci本身是一个顶层const))
auto d=&i;//d是一个整型指针(整数的地址就是指向整数的指针)
auto e=&ci;//e是一个指向整数常量的指针(对常量对象取地址是一种底层const)
如果希望推断出的auto类型是一个顶层const,需要明确指出:
const auto f=ci;//ci的推演类型是int,f是const int
-
引用
auto &g=ci;//g是一个整型常量引用,绑定到ci
auto &h=42;//错误:不能为非常量引用绑定字面值
const auto &j=42;//正确:可以为常量引用绑定字面值
设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留。和往常一样,如果我们给初始值绑定一个引用,则此时的常量就不是顶层常量了
auto k=ci,&l=i;//k是整数,1是整型引用
auto &m=ci,*p=&ci;//m是对整型常量的引用,p是指向整型常量的指针
//错误:i的类型是int而&ci的类型是const int
auto n=i,*p2=&ci;
-
decltype
-
如果我们仅仅是想根据初始值确定一个变量合适的数据类型,那么auto是最佳人选。而只有当我们需要推断某个表达式的数据类型,并将其作为一种新的数据类型重复使用(比如,定义多个相同类型变量)或者单独使用(比如,作为函数的返回值类型)时,我们才真正需要用到decltype。
-
decltype(f())sum=x;//sum的类型就是函数f的返回类型
编译器并不实际调用函数f,而是使用当调用发生时f的返回值类型作为sum的类型。
-
如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内):
const int ci=0,&cj=ci;
decltype(ci)x=0;//x的类型是const int
decltype(cj)y=x;//y的类型是const int&,y绑定到变量x
decltype(cj)z;//错误:z是一个引用,必须初始化
因为cj是一个引用,decltype(cj)的结果就是引用类型,因此作为引用的z必须被初始化。需要指出的是,引用从来都作为其所指对象的同义词出现,只有用在decltype处是一个例外。
如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。
//decltype的结果可以是引用类型
int i=42,*p=&i,&r=i;
decltype(r+0)b;//正确:加法的结果是int,因此b是一个(未初始化的)int
decltype(*p)c;//错误:c是int&,必须初始化
因为r是一个引用,因此decltype(r)的结果是引用类型。如果想让结果类型是r所指的类型,可以把r作为表达式的一部分,如r+0,显然这个表达式的结果将是一个具体值而非一个引用。另一方面,如果表达式的内容是解引用操作,则decltype将得到引用类型。正如我们所熟悉的那样,解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype(*p)的结果类型就是int&,而非int。
-
//decltype的表达式如果是加上了括号的变量,结果将是引用
decltype((i))d;//错误:d是int&,必须初始化
decltype(i)e;//正确:e是一个(未初始化的)int
decltype((variable))(注意是双层括号)的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用
自定义数据结构
-
定义
-
struct Sales data{}accum,trans,*salesptr;
//与上一条语句等价,但可能更好一些
struct Sales data{};
Sales data accum,trans,*salesptr; 分号表示声明符(通常为空)的结束。一般来说,最好不要把对象的定义和类的定义放在一起。这么做无异于把两种不同实体的定义混在了一条语句里,一会儿定义类,一会儿又定义变量,显然这是一种不被建议的行为。
-
如果一个指针指向的是一个结构体类型的变量,与结构体变量使用“.”符号引出成员变量不同的是,如果是指向结构体的指针,则应该用“->”符号引出其成员变量。
// 定义一个结构体变量
Employee Zengmei;
// 定义一个指针,并将其指向这个结构体变量
Employee* pZengmei = &Zengmei;
// 用“->”运算符,引用这个结构体指针变量的成员变量
pZengmei->m_strName = "Zengmei";
pZengmei->m_nAge = 28;
-
编写头文件
-
类一般都不定义在函数体内。当在函数体外部定义类时,在各个指定的源文件中可能只有一处该类的定义。而且,如果要在不同文件中使用同一个类,类的定义就必须保持一致。为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样。例如,库类型string在名为string的头文件中定义。又如,我们应该把Sales_data类定义在名为Sales_data.h的头文件中。头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明。
-
当预处理器看到#include标记时就会用指定的头文件的内容代替#include。C++程序还会用到的一项预处理功能是头文件保护符(headerguard),头文件保护符依赖于预处理变量(参见2.3.2节,第48页)。预处理变量有两种状态:已定义和未定义。#define指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:#ifdef当且仅当变量已定义时为真,#ifndef当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到#endif指令为止。使用这些功能就能有效地防止重复包含的发生:
#ifndef SALES DATA H
#define SALES DATA H
#include
struct Sales data{
std::string bookNo;
unsigned units sold=0;
double revenue=0.0;
};
#endif
第一次包含Sales_data.h时,#ifndef的检查结果为真,预处理器将顺序执行后面的操作直至遇到#endif为止。此时,预处理变量SALES_DATA_H的值将变为已定义,而且Sales_data.h也会被拷贝到我们的程序中来。后面如果再一次包含Sales_data.h,则#ifndef的检查结果将为假,编译器将忽略#ifndef到#endif之间的部分。
第3章 字符串、向量和数组
命名空间的using声明
-
std::cin的意思就是要使用命名空间std中的名字cin
-
using namespace::name;
using std::cin;
cin>>i;//正确:cin和std::cin含义相同
-
位于头文件的代码一般来说不应该使用using声明。这是因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个using声明,那么每个使用了该头文件的文件就都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。
标准库类型string
-
使用string类型必须首先包含string头文件
-
初始化
string s1;//默认初始化,s1是一个空字符串
string s2=s1;//s2是s1的副本
string s3="hiya";//s3是该字符串字面值的副本
tring s4(10,'c');//s4的内容是cccccccccc
string s1默认初始化,s1是一个空串
string s2(s1)s2是s1的副本
string s2=s1等价于s2(s1),s2是s1的副本
string s3("value")s3是字面值"value”的副本,除了字面值最后的那个空字符外
string s3="value”等价于s3("value"),s3是字面值“value”的副本
string s4(n,'c')把s4初始化为由连续n个字符c组成的串
-
直接初始化和拷贝初始化
-
当初始值只有一个时,使用直接初始化或拷贝初始化都行。如果像上面的s4那样初始化要用到的值有多个,一般来说只能使用直接初始化的方式
string s5="hiya";//拷贝初始化
string s6("hiya");//直接初始化
string s7(10,'c');//直接初始化,s7的内容是cccccccccc
-
对于用多个值进行初始化的情况,非要用拷贝初始化的方式来处理也不是不可以,不过需要显式地创建一个(临时)对象用于拷贝
string s8=string(10,'c');//拷贝初始化,s8的内容是cccccccccc
常用数据类型
| 常用数据类型 | 占用字节数 | 取值范围 |
|---|---|---|
| short(前面可加unsigned)【整型】 | 2 | -32768~32767(0~65535) |
| int(前面可加unsigned)【整型】 | 4 | -2147483648~2147483647(同理) |
| long(前面可加unsigned)【整型】 | 4 | -2147483648~2147483647(同理) |
| long long(前面可加unsigned)【整型】 | 8 | -9223372036854775808~…807(同理) |
| float【实型】 | 4 | 负:-3.4×10^38~-3.4×10^-38 正:3.4×10^-38~3.4×10^38 |
| double【实型】 | 8 | 负:-1.7×10^308~-1.7×10^-308 正:1.7×10^-308~1.7×10^308 |
| long double【实型】 | 10 | 负:-1.1×10^4932~-1.1×10^-4932 正:1.1×10^-4932~1.1×10^4932 |
| char(前面可加unsigned)【字符型】 | 1 | -128~127(0~255) |
| bool【布尔型】 | 1 | true,false |
类型转换
-
布尔类型:当我们把一个非布尔类型的算术值赋给布尔类型时,初始值为0则结果为false,否则结果为true。· 当我们把一个布尔值赋给非布尔类型时,初始值为false则结果为0,初始值为true则结果为1。
-
浮点数:当我们把一个浮点数赋给整数类型时,进行了近似处理。结果值将仅保留浮点数中小数点之前的部分。 当我们把一个整数值赋给浮点类型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失。
-
无符号:当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。例如,8比特大小的unsigned char可以表示0至255区间内的值,如果我们赋了一个区间以外的值,则实际的结果是该值对256取模后所得的余数。因此,把-1赋给8比特大小的unsigned char所得的结果是255。
-
字符:当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据。
-
当一个算术表达式中既有无符号数又有int值时,那个int值就会转换成无符号数
-
规则
-
隐式转换
高double⬅float
unsigned long int
long int
unsigned int
低int⬅char、short、bool
-
算术运算中转换:低到高
-
赋值运算中转换:
-
高向低:int a;a=15.5;
-
低向高:double a=10;cout<
-
-
-
显式转换:(数据类型)变量或常量、或数据类型(变量或常量或表达式)
-
常量
-
整型常量
-
二进制:0b10或者0B10
-
八进制:073
-
十进制:287
-
十八进制:0x1AF或者0x1AF
-
注意:cout << 1'100'000;分隔符不影响数值
-
-
实型常量
-
浮点表示法:-0.126
-
科学计数法:12.6E-5、126e-6→格式化:1.26E-4
-
-
字符常量
-
普通字符:‘A’(ASCII为65)
-
转义字符:
转义字符 功能 转义字符 功能 a 响铃 v 垂直制表 b 退格 ' f 换页 '' n 换行 \ r 回车(本行开头) nnn 1~3位八进制代表代表字符 t 水平制表,tab位置 xnn 十六进制代表队字符 注意:如果反斜线后面跟着的八进制数字超过3个,只有前3个数字与构成转义序列。例如,"1234"表示2个字符,即八进制数123对应的字符以及字符4。相反,x要用到后面跟着的所有数字,例如,"x1234"表示一个16位的字符,该字符由这4个十六进制数所对应的比特唯一确定。因为大多数机器的char型数据占8位,所以上面这个例子可能会报错。
-
-
字符串常量:
-
”Motnth“,长度为5,系统自动后面加‘ ’
-
string s = R"(zeng\mei)"; // 使用R"()"表示的原生字符串
-
-
指定字面值的类型
L’a'//宽字符型字面值,类型是wchar_t u8"hi!"//utf-8字符串字面值(utf-8用8位编码一个Unicode字符) 42ULL//无符号整型字面值,类型是unsigned long 1ong 1E-3F//单精度浮点型字面值,类型是float 3.14159L//扩展精度浮点型字面值,类型是long double
-
列表初始化
-
int units sold=0; int units sold={0}; int units sold{0};int units sold(0);都可以初始化和赋新值
-
当用于内置类型的变量时,这种初始化形式有一个重要特点:如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错:
long double 1d=3.1415926536; int a{ld},b={1d};//错误:转换未执行,因为存在丢失信息的危险 intc(1d),d=1d;//正确:转换执行,且确实丢失了部分值 -
默认初始值
-
如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。定义于任何函数体之外的变量被初始化为0。
-
定义于函数体内的内置类型的对象如果没有初始化,则其值未定义。类的对象如果没有显式地初始化,则其值由类确定。
-
如果试图拷贝或以其他形式访问此类值将引发错误
-
-
-
变量声明和定义
-
如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显式地初始化变量:
extern int i;//声明i而非定义i int j;//声明并定义
-
extern语句如果包含初始值就不再是声明,而变成定义了:
extern double pi=3.1416;//定义
-
变量能且只能被定义一次,但是可以被多次声明。所以,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。
-
-
标识符
-
用户自定义的标识符中不能连续出现两个下画线,也不能以下画线紧连大写字母开头。此外,定义在函数体外的标识符不能以下画线开头。
-
变量命名规范:
-
变量名一般用小写字母,如index,不要使用Index或INDEX
-
用户自定义的类名一般以大写字母开头,如Sales_item
-
如果标识符由多个单词组成,则单词间应有明显区分,如student_loan或studentLoan,不要使用studentloan
-
-
如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量
-
复合类型
-
引用(左值引用)
-
引用即别名:一般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值绑定(bind)在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。
int ival=1024;
int &refVal=ival;//refVal指向ival(是ival的另一个名字)
int &refVal2;//报错:引用必须被初始化
-
定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的:
refVal=2;//把2赋给refVal指向的对象,此处即是赋给了ival
int ii=refVal;//与ii=ival执行结果一样
-
因为引用本身不是一个对象,所以不能定义引用的引用
-
允许在一条语句中定义多个引用,其中每个引用标识符都必须以符号&开头:
int i=1024,i2=2048;//i和i2都是int
int &r=i,r2=i2;//r是一个引用,与i绑定在一起,r2是int
int i3=1024,&ri=i3;//i3是int,ri是一个引用,与i3绑定在一起
int &r3=i3,&r4=i2;//r3和r4都是引用
-
指针
-
指针与引用相比有很多不同点。其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。其二,指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
-
如果在一条语句中定义了几个指针变量,每个变量前面都必须有符号*
int*ipl,*ip2;//ipl和ip2都是指向int型对象的指针
double dp,*dp2;//dp2是指向double型对象的指针,dp是double型对象
-
因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。
-
空指针:
-
空指针形成方法:
int*pl=nullptr;//等价于int*pl=0;
int*p2=0;//直接将p2初始化为字面常量0
//需要首先#include cstdlib
int*p3=NULL;//等价于int*p3=0;
-
建议:初始化所有指针,如果实在不清楚指针应该指向何处,就把它初始化为nullptr或者0,这样程序就能检测并知道它没有指向任何具体的对象了
-
void *指针:是一种特殊的指针类型,可用于存放任意对象的地址。利用void*指针能做的事儿比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void*指针。不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。概括说来,以void*的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象。
-
理解复合类型的声明
-
一条定义语句可能定义出不同类型的变量:
//i是一个int型的数,p是一个int型指针,r是一个int型引用
int i=1024,*p=&i,&r=i;
-
int* p;//合法但是容易产生误导
我们说这种写法可能产生误导是因为int*放在一起好像是这条语句中所有变量共同的类型一样。其实恰恰相反,基本数据类型是int而非int*。*仅仅是修饰了p而已,对该声明语句中的其他变量,它并不产生任何作用:
int* pl,p2;//p1是指向int的指针,p2是int
-
指向指针的指针
int ival=1024;
int *pi=&ival;//pi指向一个int型的数
int **ppi=&pi;//ppi指向一个int型的指针
-
引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用:
int i=42;
int*p;//p是一个int型指针
int *&r=p;//r是一个对指针p的引用
r=&i;//r引用了一个指针,因此给r赋值&i就是令p指向i
*r=0;//解引用r得到i,也就是p指向的对象,将i的值改为0
要理解r的类型到底是什么,最简单的办法是从右向左阅读r的定义。离变量名最近的符号(此例中是&r的符号&)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针。最后,声明的基本数据类型部分指出r引用的是一个int指针
const限定符
-
初始化
-
因为const对象一旦创建后其值就不能再改变,所以const对象必须初始化
const inti=get size();//正确:运行时初始化
const intj=42;//正确:编译时初始化
const int k;//错误:k是一个未经初始化的常量
-
同时避免对同一变量的重复定义,默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量
-
如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字:
//file1.cc定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufsize=fcn();
//file1.h头文件
extern const int bufsize;//与file 1.cc中定义的bufsize是同一个
-
const的引用
-
const int ci=1024;
const int &rl=ci;//正确:引用及其对应的对象都是常量
r1=42;//错误:r1是对常量的引用
int &r2=ci;//错误:试图让一个非常量引用指向一个常量对象
-
C++程序员们经常把词组“对const的引用”简称为“常量引用”,这一简称还是挺靠谱的,不过前提是你得时刻记得这就是个简称而已。严格来说,并不存在常量引用。因为引用不是一个对象,所以我们没法让引用本身恒定不变。事实上,由于C++语言并不允许随意改变引用所绑定的对象,所以从这层意义上理解所有的引用又都算是常量。
-
引用的类型必须与其所引用对象的类型一致,但是有两个例外。第一种例外情况就是在初始化常量引用时允许用任意表达式作为初始值(临时常量=i;常量引用=;临时常量)
int i=42;
const int&rl=i;//允许将const ints绑定到一个普通int对象上
const int&r2=42;//正确:r1是一个常量引用
const int&r3=r1*2;//正确:r3是一个常量引用
int &r4=r1*2;//错误:r4是一个普通的非常量引用
r2绑定(非常量)整数i是合法的行为。然而,不允许通过r2修改i的值。尽管如此,i的值仍然允许通过其他途径修改,既可以直接给i赋值,也可以通过绑定到i的其他引用来修改
-
const double pi=3.14;//pi是个常量,它的值不能改变
double *ptr=&pi;//错误:ptr是一个普通指针
const double*cptr=&pi;//正确:cptr可以指向一个双精度常量
*Cptr=42;//错误:不能给*cptr赋值
-
指针的类型必须与其所指对象的类型一致,但是有两个例外。第一种例外情况是允许令一个指向常量的指针指向一个非常量对象。和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变
-
指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定为常量。常量指针(const pointer)必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。把*放在const关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味,即不变的是指针本身的值而非指向的那个值:
int errNumb=0;
int *const curErr=&errNumb;//curErr将一直指向errNumb
const double pi=3.14159;
const double *const pip=&pi;//pip是一个指向常量对象的常量指针
可以通过curErr修改errNumb值,不可以通过pip改变pi
-
顶层const和底层const
-
用名词顶层const(top-level const)表示指针本身是个常量,而用名词底层const(low-level const)表示指针所指的对象是一个常量
inti=0;
int *const pl=&i;//不能改变p1的值,这是一个顶层const
const int ci=42;//不能改变ci的值,这是一个顶层const
const int*p2=&ci;//允许改变p2的值,这是一个底层const
const int*const p3=p2;//靠右的 const 是顶层const,靠左的是底层const
const int&r=ci;//用于声明引用的 const 都是底层const
-
当执行对象的拷贝操作时,常量是顶层const还是底层const区别明显。其中,顶层const不受什么影响:
i=ci;//正确:拷贝ci的值,ci是一个顶层const,对此操作无影响
p2=p3;//正确:p2和p3指向的对象类型相同,p3顶层const的部分不影响
-
底层const的限制却不能忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之则不行:
int *p=p3;//错误:p3包含底层const的定义,而p没有
p2=p3;//正确:p2和p3都是底层const
p2=&i;//正确:int*能转换成const int*
int &r=ci;//错误:普通的int&不能绑定到int常量上
const int&r2=i;//正确:const int&可以绑定到一个普通int上
p3既是顶层const也是底层const,拷贝p3时可以不在乎它是一个顶层const,但是必须清楚它指向的对象得是一个常量。因此,不能用p3去初始化p,因为p指向的是一个普通的(非常量)整数。另一方面,p3的值可以赋给p2,是因为这两个指针都是底层const,尽管p3同时也是一个常量指针(顶层const),仅就这次赋值而言不会有什么影响
-
常量表达式
-
常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式
const int max files=20;//max files是常量表达式
const int limit=max files+1;//limit是常量表达式
int staff_size=27;//staff_size不是常量表达式
const int sz=get_size();//sz不是常量表达式
尽管sz本身是一个常量,但它的具体值直到运行时才能获取到,所以不是常量表达式
-
C++11 标准中,定义变量时可以用 constexpr 修饰,从而使该变量获得在编译阶段即可计算出结果的能力。常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为“字面值类型”。算术类型、引用和指针都属于字面值类型。自定义类Sales_item、IO库、string类型则不属于字面值类型,也就不能被定义成constexpr。尽管指针和引用都能定义成constexpr,但它们的初始值却受到严格限制。一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象。(函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。同样是在6.1.1节(第185页)中还将提到,允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址。因此,constexpr引用能绑定到这样的变量上,constexpr指针也能指向这样的变量)新标准允许定义一种特殊的constexpr函数。这种函数应该足够简单以使得编译时就可以计算其结果,这样就能用constexpr函数去初始化constexpr变量了。
处理类型
-
类型别名
-
方法一:
typedef double wages;//wages是double的同义词
typedef wages base,*p;//base是double的同义词,p是double*的同义词
-
方法二:
using SI=Sales item;//SI是Sales item的同义词
-
typedef char *pstring;
const pstring cstr=0;//cstr是指向char的常量指针
const pstring *ps;//ps是一个指针,它的对象是指向char的常量指针
char*别名是pstring
const char *cstr=0;//是对const pstring cstr的错误理解
声明语句中用到pstring时,其基本数据类型是指针。可是用char*重写了声明语句后,数据类型就变成了char,*成为了声明符的一部分。这样改写的结果是,const char成了基本数据类型。前后两种声明含义截然不同,前者声明了一个指向char的常量指针,改写后的形式则声明了一个指向const char的指针。
-
auto
-
C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。显然,auto定义的变量必须有初始值。
-
使用auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:
auto i=0,*p=&i;//正确:i是整数、p是整型指针
auto sz=0,pi=3.14;//错误:sz和pi的类型不一致
-
auto一般会忽略掉顶层const(参见2.4.3节,第57页),同时底层const则会保留下来,比如当初始值是一个指向常量的指针时:
const int ci=i,&cr=ci;
auto b=ci;//b是一个整数(ci的顶层const特性被忽略掉了)
auto c=cr;//c是一个整数(cr是ci的别名,ci本身是一个顶层const))
auto d=&i;//d是一个整型指针(整数的地址就是指向整数的指针)
auto e=&ci;//e是一个指向整数常量的指针(对常量对象取地址是一种底层const)
如果希望推断出的auto类型是一个顶层const,需要明确指出:
const auto f=ci;//ci的推演类型是int,f是const int
-
引用
auto &g=ci;//g是一个整型常量引用,绑定到ci
auto &h=42;//错误:不能为非常量引用绑定字面值
const auto &j=42;//正确:可以为常量引用绑定字面值
设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留。和往常一样,如果我们给初始值绑定一个引用,则此时的常量就不是顶层常量了
auto k=ci,&l=i;//k是整数,1是整型引用
auto &m=ci,*p=&ci;//m是对整型常量的引用,p是指向整型常量的指针
//错误:i的类型是int而&ci的类型是const int
auto n=i,*p2=&ci;
-
decltype
-
如果我们仅仅是想根据初始值确定一个变量合适的数据类型,那么auto是最佳人选。而只有当我们需要推断某个表达式的数据类型,并将其作为一种新的数据类型重复使用(比如,定义多个相同类型变量)或者单独使用(比如,作为函数的返回值类型)时,我们才真正需要用到decltype。
-
decltype(f())sum=x;//sum的类型就是函数f的返回类型
编译器并不实际调用函数f,而是使用当调用发生时f的返回值类型作为sum的类型。
-
如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内):
const int ci=0,&cj=ci;
decltype(ci)x=0;//x的类型是const int
decltype(cj)y=x;//y的类型是const int&,y绑定到变量x
decltype(cj)z;//错误:z是一个引用,必须初始化
因为cj是一个引用,decltype(cj)的结果就是引用类型,因此作为引用的z必须被初始化。需要指出的是,引用从来都作为其所指对象的同义词出现,只有用在decltype处是一个例外。
如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。
//decltype的结果可以是引用类型
int i=42,*p=&i,&r=i;
decltype(r+0)b;//正确:加法的结果是int,因此b是一个(未初始化的)int
decltype(*p)c;//错误:c是int&,必须初始化
因为r是一个引用,因此decltype(r)的结果是引用类型。如果想让结果类型是r所指的类型,可以把r作为表达式的一部分,如r+0,显然这个表达式的结果将是一个具体值而非一个引用。另一方面,如果表达式的内容是解引用操作,则decltype将得到引用类型。正如我们所熟悉的那样,解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype(*p)的结果类型就是int&,而非int。
-
//decltype的表达式如果是加上了括号的变量,结果将是引用
decltype((i))d;//错误:d是int&,必须初始化
decltype(i)e;//正确:e是一个(未初始化的)int
decltype((variable))(注意是双层括号)的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用
自定义数据结构
-
定义
-
struct Sales data{}accum,trans,*salesptr;
//与上一条语句等价,但可能更好一些
struct Sales data{};
Sales data accum,trans,*salesptr; 分号表示声明符(通常为空)的结束。一般来说,最好不要把对象的定义和类的定义放在一起。这么做无异于把两种不同实体的定义混在了一条语句里,一会儿定义类,一会儿又定义变量,显然这是一种不被建议的行为。
-
如果一个指针指向的是一个结构体类型的变量,与结构体变量使用“.”符号引出成员变量不同的是,如果是指向结构体的指针,则应该用“->”符号引出其成员变量。
// 定义一个结构体变量
Employee Zengmei;
// 定义一个指针,并将其指向这个结构体变量
Employee* pZengmei = &Zengmei;
// 用“->”运算符,引用这个结构体指针变量的成员变量
pZengmei->m_strName = "Zengmei";
pZengmei->m_nAge = 28;
-
编写头文件
-
类一般都不定义在函数体内。当在函数体外部定义类时,在各个指定的源文件中可能只有一处该类的定义。而且,如果要在不同文件中使用同一个类,类的定义就必须保持一致。为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样。例如,库类型string在名为string的头文件中定义。又如,我们应该把Sales_data类定义在名为Sales_data.h的头文件中。头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明。
-
当预处理器看到#include标记时就会用指定的头文件的内容代替#include。C++程序还会用到的一项预处理功能是头文件保护符(headerguard),头文件保护符依赖于预处理变量(参见2.3.2节,第48页)。预处理变量有两种状态:已定义和未定义。#define指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:#ifdef当且仅当变量已定义时为真,#ifndef当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到#endif指令为止。使用这些功能就能有效地防止重复包含的发生:
#ifndef SALES DATA H
#define SALES DATA H
#include
struct Sales data{
std::string bookNo;
unsigned units sold=0;
double revenue=0.0;
};
#endif
第一次包含Sales_data.h时,#ifndef的检查结果为真,预处理器将顺序执行后面的操作直至遇到#endif为止。此时,预处理变量SALES_DATA_H的值将变为已定义,而且Sales_data.h也会被拷贝到我们的程序中来。后面如果再一次包含Sales_data.h,则#ifndef的检查结果将为假,编译器将忽略#ifndef到#endif之间的部分。
第3章 字符串、向量和数组
命名空间的using声明
-
std::cin的意思就是要使用命名空间std中的名字cin
-
using namespace::name;
using std::cin;
cin>>i;//正确:cin和std::cin含义相同
-
位于头文件的代码一般来说不应该使用using声明。这是因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个using声明,那么每个使用了该头文件的文件就都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。
标准库类型string
-
使用string类型必须首先包含string头文件
-
初始化
string s1;//默认初始化,s1是一个空字符串
string s2=s1;//s2是s1的副本
string s3="hiya";//s3是该字符串字面值的副本
tring s4(10,'c');//s4的内容是cccccccccc
string s1默认初始化,s1是一个空串
string s2(s1)s2是s1的副本
string s2=s1等价于s2(s1),s2是s1的副本
string s3("value")s3是字面值"value”的副本,除了字面值最后的那个空字符外
string s3="value”等价于s3("value"),s3是字面值“value”的副本
string s4(n,'c')把s4初始化为由连续n个字符c组成的串
-
直接初始化和拷贝初始化
-
当初始值只有一个时,使用直接初始化或拷贝初始化都行。如果像上面的s4那样初始化要用到的值有多个,一般来说只能使用直接初始化的方式
string s5="hiya";//拷贝初始化
string s6("hiya");//直接初始化
string s7(10,'c');//直接初始化,s7的内容是cccccccccc
-
对于用多个值进行初始化的情况,非要用拷贝初始化的方式来处理也不是不可以,不过需要显式地创建一个(临时)对象用于拷贝
string s8=string(10,'c');//拷贝初始化,s8的内容是cccccccccc
引用(左值引用)
-
引用即别名:一般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值绑定(bind)在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。
int ival=1024; int &refVal=ival;//refVal指向ival(是ival的另一个名字) int &refVal2;//报错:引用必须被初始化
-
定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的:
refVal=2;//把2赋给refVal指向的对象,此处即是赋给了ival int ii=refVal;//与ii=ival执行结果一样
-
因为引用本身不是一个对象,所以不能定义引用的引用
-
允许在一条语句中定义多个引用,其中每个引用标识符都必须以符号&开头:
int i=1024,i2=2048;//i和i2都是int int &r=i,r2=i2;//r是一个引用,与i绑定在一起,r2是int int i3=1024,&ri=i3;//i3是int,ri是一个引用,与i3绑定在一起 int &r3=i3,&r4=i2;//r3和r4都是引用
指针
-
指针与引用相比有很多不同点。其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。其二,指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
-
如果在一条语句中定义了几个指针变量,每个变量前面都必须有符号*
int*ipl,*ip2;//ipl和ip2都是指向int型对象的指针 double dp,*dp2;//dp2是指向double型对象的指针,dp是double型对象
-
因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。
-
空指针:
-
空指针形成方法:
int*pl=nullptr;//等价于int*pl=0; int*p2=0;//直接将p2初始化为字面常量0 //需要首先#include cstdlib int*p3=NULL;//等价于int*p3=0;
-
-
建议:初始化所有指针,如果实在不清楚指针应该指向何处,就把它初始化为nullptr或者0,这样程序就能检测并知道它没有指向任何具体的对象了
-
void *指针:是一种特殊的指针类型,可用于存放任意对象的地址。利用void*指针能做的事儿比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void*指针。不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。概括说来,以void*的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象。
理解复合类型的声明
-
一条定义语句可能定义出不同类型的变量:
//i是一个int型的数,p是一个int型指针,r是一个int型引用 int i=1024,*p=&i,&r=i;
-
int* p;//合法但是容易产生误导
我们说这种写法可能产生误导是因为int*放在一起好像是这条语句中所有变量共同的类型一样。其实恰恰相反,基本数据类型是int而非int*。*仅仅是修饰了p而已,对该声明语句中的其他变量,它并不产生任何作用:
int* pl,p2;//p1是指向int的指针,p2是int
-
指向指针的指针
int ival=1024; int *pi=&ival;//pi指向一个int型的数 int **ppi=&pi;//ppi指向一个int型的指针
-
引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用:
int i=42; int*p;//p是一个int型指针 int *&r=p;//r是一个对指针p的引用 r=&i;//r引用了一个指针,因此给r赋值&i就是令p指向i *r=0;//解引用r得到i,也就是p指向的对象,将i的值改为0
要理解r的类型到底是什么,最简单的办法是从右向左阅读r的定义。离变量名最近的符号(此例中是&r的符号&)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针。最后,声明的基本数据类型部分指出r引用的是一个int指针
-
初始化
-
因为const对象一旦创建后其值就不能再改变,所以const对象必须初始化
const inti=get size();//正确:运行时初始化 const intj=42;//正确:编译时初始化 const int k;//错误:k是一个未经初始化的常量
-
同时避免对同一变量的重复定义,默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量
-
如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字:
//file1.cc定义并初始化了一个常量,该常量能被其他文件访问 extern const int bufsize=fcn(); //file1.h头文件 extern const int bufsize;//与file 1.cc中定义的bufsize是同一个
-
-
const的引用
-
const int ci=1024; const int &rl=ci;//正确:引用及其对应的对象都是常量 r1=42;//错误:r1是对常量的引用 int &r2=ci;//错误:试图让一个非常量引用指向一个常量对象
-
C++程序员们经常把词组“对const的引用”简称为“常量引用”,这一简称还是挺靠谱的,不过前提是你得时刻记得这就是个简称而已。严格来说,并不存在常量引用。因为引用不是一个对象,所以我们没法让引用本身恒定不变。事实上,由于C++语言并不允许随意改变引用所绑定的对象,所以从这层意义上理解所有的引用又都算是常量。
-
引用的类型必须与其所引用对象的类型一致,但是有两个例外。第一种例外情况就是在初始化常量引用时允许用任意表达式作为初始值(临时常量=i;常量引用=;临时常量)
int i=42; const int&rl=i;//允许将const ints绑定到一个普通int对象上 const int&r2=42;//正确:r1是一个常量引用 const int&r3=r1*2;//正确:r3是一个常量引用 int &r4=r1*2;//错误:r4是一个普通的非常量引用
r2绑定(非常量)整数i是合法的行为。然而,不允许通过r2修改i的值。尽管如此,i的值仍然允许通过其他途径修改,既可以直接给i赋值,也可以通过绑定到i的其他引用来修改
-
const double pi=3.14;//pi是个常量,它的值不能改变 double *ptr=&pi;//错误:ptr是一个普通指针 const double*cptr=&pi;//正确:cptr可以指向一个双精度常量 *Cptr=42;//错误:不能给*cptr赋值
-
指针的类型必须与其所指对象的类型一致,但是有两个例外。第一种例外情况是允许令一个指向常量的指针指向一个非常量对象。和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变
-
-
指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定为常量。常量指针(const pointer)必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。把*放在const关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味,即不变的是指针本身的值而非指向的那个值:
int errNumb=0; int *const curErr=&errNumb;//curErr将一直指向errNumb const double pi=3.14159; const double *const pip=&pi;//pip是一个指向常量对象的常量指针
可以通过curErr修改errNumb值,不可以通过pip改变pi
-
顶层const和底层const
-
用名词顶层const(top-level const)表示指针本身是个常量,而用名词底层const(low-level const)表示指针所指的对象是一个常量
inti=0; int *const pl=&i;//不能改变p1的值,这是一个顶层const const int ci=42;//不能改变ci的值,这是一个顶层const const int*p2=&ci;//允许改变p2的值,这是一个底层const const int*const p3=p2;//靠右的 const 是顶层const,靠左的是底层const const int&r=ci;//用于声明引用的 const 都是底层const
-
当执行对象的拷贝操作时,常量是顶层const还是底层const区别明显。其中,顶层const不受什么影响:
i=ci;//正确:拷贝ci的值,ci是一个顶层const,对此操作无影响 p2=p3;//正确:p2和p3指向的对象类型相同,p3顶层const的部分不影响
-
底层const的限制却不能忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之则不行:
int *p=p3;//错误:p3包含底层const的定义,而p没有 p2=p3;//正确:p2和p3都是底层const p2=&i;//正确:int*能转换成const int* int &r=ci;//错误:普通的int&不能绑定到int常量上 const int&r2=i;//正确:const int&可以绑定到一个普通int上
p3既是顶层const也是底层const,拷贝p3时可以不在乎它是一个顶层const,但是必须清楚它指向的对象得是一个常量。因此,不能用p3去初始化p,因为p指向的是一个普通的(非常量)整数。另一方面,p3的值可以赋给p2,是因为这两个指针都是底层const,尽管p3同时也是一个常量指针(顶层const),仅就这次赋值而言不会有什么影响
-
-
常量表达式
-
常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式
const int max files=20;//max files是常量表达式 const int limit=max files+1;//limit是常量表达式 int staff_size=27;//staff_size不是常量表达式 const int sz=get_size();//sz不是常量表达式
尽管sz本身是一个常量,但它的具体值直到运行时才能获取到,所以不是常量表达式
-
C++11 标准中,定义变量时可以用 constexpr 修饰,从而使该变量获得在编译阶段即可计算出结果的能力。常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为“字面值类型”。算术类型、引用和指针都属于字面值类型。自定义类Sales_item、IO库、string类型则不属于字面值类型,也就不能被定义成constexpr。尽管指针和引用都能定义成constexpr,但它们的初始值却受到严格限制。一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象。(函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。同样是在6.1.1节(第185页)中还将提到,允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址。因此,constexpr引用能绑定到这样的变量上,constexpr指针也能指向这样的变量)新标准允许定义一种特殊的constexpr函数。这种函数应该足够简单以使得编译时就可以计算其结果,这样就能用constexpr函数去初始化constexpr变量了。
-
处理类型
-
类型别名
-
方法一:
typedef double wages;//wages是double的同义词
typedef wages base,*p;//base是double的同义词,p是double*的同义词
-
方法二:
using SI=Sales item;//SI是Sales item的同义词
-
typedef char *pstring;
const pstring cstr=0;//cstr是指向char的常量指针
const pstring *ps;//ps是一个指针,它的对象是指向char的常量指针
char*别名是pstring
const char *cstr=0;//是对const pstring cstr的错误理解
声明语句中用到pstring时,其基本数据类型是指针。可是用char*重写了声明语句后,数据类型就变成了char,*成为了声明符的一部分。这样改写的结果是,const char成了基本数据类型。前后两种声明含义截然不同,前者声明了一个指向char的常量指针,改写后的形式则声明了一个指向const char的指针。
-
auto
-
C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。显然,auto定义的变量必须有初始值。
-
使用auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:
auto i=0,*p=&i;//正确:i是整数、p是整型指针
auto sz=0,pi=3.14;//错误:sz和pi的类型不一致
-
auto一般会忽略掉顶层const(参见2.4.3节,第57页),同时底层const则会保留下来,比如当初始值是一个指向常量的指针时:
const int ci=i,&cr=ci;
auto b=ci;//b是一个整数(ci的顶层const特性被忽略掉了)
auto c=cr;//c是一个整数(cr是ci的别名,ci本身是一个顶层const))
auto d=&i;//d是一个整型指针(整数的地址就是指向整数的指针)
auto e=&ci;//e是一个指向整数常量的指针(对常量对象取地址是一种底层const)
如果希望推断出的auto类型是一个顶层const,需要明确指出:
const auto f=ci;//ci的推演类型是int,f是const int
-
引用
auto &g=ci;//g是一个整型常量引用,绑定到ci
auto &h=42;//错误:不能为非常量引用绑定字面值
const auto &j=42;//正确:可以为常量引用绑定字面值
设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留。和往常一样,如果我们给初始值绑定一个引用,则此时的常量就不是顶层常量了
auto k=ci,&l=i;//k是整数,1是整型引用
auto &m=ci,*p=&ci;//m是对整型常量的引用,p是指向整型常量的指针
//错误:i的类型是int而&ci的类型是const int
auto n=i,*p2=&ci;
-
decltype
-
如果我们仅仅是想根据初始值确定一个变量合适的数据类型,那么auto是最佳人选。而只有当我们需要推断某个表达式的数据类型,并将其作为一种新的数据类型重复使用(比如,定义多个相同类型变量)或者单独使用(比如,作为函数的返回值类型)时,我们才真正需要用到decltype。
-
decltype(f())sum=x;//sum的类型就是函数f的返回类型
编译器并不实际调用函数f,而是使用当调用发生时f的返回值类型作为sum的类型。
-
如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内):
const int ci=0,&cj=ci;
decltype(ci)x=0;//x的类型是const int
decltype(cj)y=x;//y的类型是const int&,y绑定到变量x
decltype(cj)z;//错误:z是一个引用,必须初始化
因为cj是一个引用,decltype(cj)的结果就是引用类型,因此作为引用的z必须被初始化。需要指出的是,引用从来都作为其所指对象的同义词出现,只有用在decltype处是一个例外。
如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。
//decltype的结果可以是引用类型
int i=42,*p=&i,&r=i;
decltype(r+0)b;//正确:加法的结果是int,因此b是一个(未初始化的)int
decltype(*p)c;//错误:c是int&,必须初始化
因为r是一个引用,因此decltype(r)的结果是引用类型。如果想让结果类型是r所指的类型,可以把r作为表达式的一部分,如r+0,显然这个表达式的结果将是一个具体值而非一个引用。另一方面,如果表达式的内容是解引用操作,则decltype将得到引用类型。正如我们所熟悉的那样,解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype(*p)的结果类型就是int&,而非int。
-
//decltype的表达式如果是加上了括号的变量,结果将是引用
decltype((i))d;//错误:d是int&,必须初始化
decltype(i)e;//正确:e是一个(未初始化的)int
decltype((variable))(注意是双层括号)的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用
自定义数据结构
-
定义
-
struct Sales data{}accum,trans,*salesptr;
//与上一条语句等价,但可能更好一些
struct Sales data{};
Sales data accum,trans,*salesptr; 分号表示声明符(通常为空)的结束。一般来说,最好不要把对象的定义和类的定义放在一起。这么做无异于把两种不同实体的定义混在了一条语句里,一会儿定义类,一会儿又定义变量,显然这是一种不被建议的行为。
-
如果一个指针指向的是一个结构体类型的变量,与结构体变量使用“.”符号引出成员变量不同的是,如果是指向结构体的指针,则应该用“->”符号引出其成员变量。
// 定义一个结构体变量
Employee Zengmei;
// 定义一个指针,并将其指向这个结构体变量
Employee* pZengmei = &Zengmei;
// 用“->”运算符,引用这个结构体指针变量的成员变量
pZengmei->m_strName = "Zengmei";
pZengmei->m_nAge = 28;
-
编写头文件
-
类一般都不定义在函数体内。当在函数体外部定义类时,在各个指定的源文件中可能只有一处该类的定义。而且,如果要在不同文件中使用同一个类,类的定义就必须保持一致。为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样。例如,库类型string在名为string的头文件中定义。又如,我们应该把Sales_data类定义在名为Sales_data.h的头文件中。头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明。
-
当预处理器看到#include标记时就会用指定的头文件的内容代替#include。C++程序还会用到的一项预处理功能是头文件保护符(headerguard),头文件保护符依赖于预处理变量(参见2.3.2节,第48页)。预处理变量有两种状态:已定义和未定义。#define指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:#ifdef当且仅当变量已定义时为真,#ifndef当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到#endif指令为止。使用这些功能就能有效地防止重复包含的发生:
#ifndef SALES DATA H
#define SALES DATA H
#include
struct Sales data{
std::string bookNo;
unsigned units sold=0;
double revenue=0.0;
};
#endif
第一次包含Sales_data.h时,#ifndef的检查结果为真,预处理器将顺序执行后面的操作直至遇到#endif为止。此时,预处理变量SALES_DATA_H的值将变为已定义,而且Sales_data.h也会被拷贝到我们的程序中来。后面如果再一次包含Sales_data.h,则#ifndef的检查结果将为假,编译器将忽略#ifndef到#endif之间的部分。
第3章 字符串、向量和数组
命名空间的using声明
-
std::cin的意思就是要使用命名空间std中的名字cin
-
using namespace::name;
using std::cin;
cin>>i;//正确:cin和std::cin含义相同
-
位于头文件的代码一般来说不应该使用using声明。这是因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个using声明,那么每个使用了该头文件的文件就都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。
标准库类型string
-
使用string类型必须首先包含string头文件
-
初始化
string s1;//默认初始化,s1是一个空字符串
string s2=s1;//s2是s1的副本
string s3="hiya";//s3是该字符串字面值的副本
tring s4(10,'c');//s4的内容是cccccccccc
string s1默认初始化,s1是一个空串
string s2(s1)s2是s1的副本
string s2=s1等价于s2(s1),s2是s1的副本
string s3("value")s3是字面值"value”的副本,除了字面值最后的那个空字符外
string s3="value”等价于s3("value"),s3是字面值“value”的副本
string s4(n,'c')把s4初始化为由连续n个字符c组成的串
-
直接初始化和拷贝初始化
-
当初始值只有一个时,使用直接初始化或拷贝初始化都行。如果像上面的s4那样初始化要用到的值有多个,一般来说只能使用直接初始化的方式
string s5="hiya";//拷贝初始化
string s6("hiya");//直接初始化
string s7(10,'c');//直接初始化,s7的内容是cccccccccc
-
对于用多个值进行初始化的情况,非要用拷贝初始化的方式来处理也不是不可以,不过需要显式地创建一个(临时)对象用于拷贝
string s8=string(10,'c');//拷贝初始化,s8的内容是cccccccccc
类型别名
-
方法一:
typedef double wages;//wages是double的同义词 typedef wages base,*p;//base是double的同义词,p是double*的同义词
-
方法二:
using SI=Sales item;//SI是Sales item的同义词
-
typedef char *pstring; const pstring cstr=0;//cstr是指向char的常量指针 const pstring *ps;//ps是一个指针,它的对象是指向char的常量指针
char*别名是pstring
const char *cstr=0;//是对const pstring cstr的错误理解
声明语句中用到pstring时,其基本数据类型是指针。可是用char*重写了声明语句后,数据类型就变成了char,*成为了声明符的一部分。这样改写的结果是,const char成了基本数据类型。前后两种声明含义截然不同,前者声明了一个指向char的常量指针,改写后的形式则声明了一个指向const char的指针。
auto
-
C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。显然,auto定义的变量必须有初始值。
-
使用auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:
auto i=0,*p=&i;//正确:i是整数、p是整型指针 auto sz=0,pi=3.14;//错误:sz和pi的类型不一致
-
auto一般会忽略掉顶层const(参见2.4.3节,第57页),同时底层const则会保留下来,比如当初始值是一个指向常量的指针时:
const int ci=i,&cr=ci; auto b=ci;//b是一个整数(ci的顶层const特性被忽略掉了) auto c=cr;//c是一个整数(cr是ci的别名,ci本身是一个顶层const)) auto d=&i;//d是一个整型指针(整数的地址就是指向整数的指针) auto e=&ci;//e是一个指向整数常量的指针(对常量对象取地址是一种底层const)
如果希望推断出的auto类型是一个顶层const,需要明确指出:
const auto f=ci;//ci的推演类型是int,f是const int
-
引用
auto &g=ci;//g是一个整型常量引用,绑定到ci auto &h=42;//错误:不能为非常量引用绑定字面值 const auto &j=42;//正确:可以为常量引用绑定字面值
设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留。和往常一样,如果我们给初始值绑定一个引用,则此时的常量就不是顶层常量了
auto k=ci,&l=i;//k是整数,1是整型引用 auto &m=ci,*p=&ci;//m是对整型常量的引用,p是指向整型常量的指针 //错误:i的类型是int而&ci的类型是const int auto n=i,*p2=&ci;
decltype
-
如果我们仅仅是想根据初始值确定一个变量合适的数据类型,那么auto是最佳人选。而只有当我们需要推断某个表达式的数据类型,并将其作为一种新的数据类型重复使用(比如,定义多个相同类型变量)或者单独使用(比如,作为函数的返回值类型)时,我们才真正需要用到decltype。
-
decltype(f())sum=x;//sum的类型就是函数f的返回类型
编译器并不实际调用函数f,而是使用当调用发生时f的返回值类型作为sum的类型。
-
如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内):
const int ci=0,&cj=ci; decltype(ci)x=0;//x的类型是const int decltype(cj)y=x;//y的类型是const int&,y绑定到变量x decltype(cj)z;//错误:z是一个引用,必须初始化
因为cj是一个引用,decltype(cj)的结果就是引用类型,因此作为引用的z必须被初始化。需要指出的是,引用从来都作为其所指对象的同义词出现,只有用在decltype处是一个例外。
如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。
//decltype的结果可以是引用类型 int i=42,*p=&i,&r=i; decltype(r+0)b;//正确:加法的结果是int,因此b是一个(未初始化的)int decltype(*p)c;//错误:c是int&,必须初始化
因为r是一个引用,因此decltype(r)的结果是引用类型。如果想让结果类型是r所指的类型,可以把r作为表达式的一部分,如r+0,显然这个表达式的结果将是一个具体值而非一个引用。另一方面,如果表达式的内容是解引用操作,则decltype将得到引用类型。正如我们所熟悉的那样,解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype(*p)的结果类型就是int&,而非int。
-
//decltype的表达式如果是加上了括号的变量,结果将是引用 decltype((i))d;//错误:d是int&,必须初始化 decltype(i)e;//正确:e是一个(未初始化的)int
decltype((variable))(注意是双层括号)的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用
-
定义
-
struct Sales data{}accum,trans,*salesptr; //与上一条语句等价,但可能更好一些 struct Sales data{}; Sales data accum,trans,*salesptr;分号表示声明符(通常为空)的结束。一般来说,最好不要把对象的定义和类的定义放在一起。这么做无异于把两种不同实体的定义混在了一条语句里,一会儿定义类,一会儿又定义变量,显然这是一种不被建议的行为。
-
如果一个指针指向的是一个结构体类型的变量,与结构体变量使用“.”符号引出成员变量不同的是,如果是指向结构体的指针,则应该用“->”符号引出其成员变量。
// 定义一个结构体变量 Employee Zengmei; // 定义一个指针,并将其指向这个结构体变量 Employee* pZengmei = &Zengmei; // 用“->”运算符,引用这个结构体指针变量的成员变量 pZengmei->m_strName = "Zengmei"; pZengmei->m_nAge = 28;
-
-
编写头文件
-
类一般都不定义在函数体内。当在函数体外部定义类时,在各个指定的源文件中可能只有一处该类的定义。而且,如果要在不同文件中使用同一个类,类的定义就必须保持一致。为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样。例如,库类型string在名为string的头文件中定义。又如,我们应该把Sales_data类定义在名为Sales_data.h的头文件中。头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明。
-
当预处理器看到#include标记时就会用指定的头文件的内容代替#include。C++程序还会用到的一项预处理功能是头文件保护符(headerguard),头文件保护符依赖于预处理变量(参见2.3.2节,第48页)。预处理变量有两种状态:已定义和未定义。#define指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:#ifdef当且仅当变量已定义时为真,#ifndef当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到#endif指令为止。使用这些功能就能有效地防止重复包含的发生:
#ifndef SALES DATA H #define SALES DATA H #include
struct Sales data{ std::string bookNo; unsigned units sold=0; double revenue=0.0; }; #endif 第一次包含Sales_data.h时,#ifndef的检查结果为真,预处理器将顺序执行后面的操作直至遇到#endif为止。此时,预处理变量SALES_DATA_H的值将变为已定义,而且Sales_data.h也会被拷贝到我们的程序中来。后面如果再一次包含Sales_data.h,则#ifndef的检查结果将为假,编译器将忽略#ifndef到#endif之间的部分。
-
第3章 字符串、向量和数组
命名空间的using声明
-
std::cin的意思就是要使用命名空间std中的名字cin
-
using namespace::name;
using std::cin;
cin>>i;//正确:cin和std::cin含义相同
-
位于头文件的代码一般来说不应该使用using声明。这是因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个using声明,那么每个使用了该头文件的文件就都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。
标准库类型string
-
使用string类型必须首先包含string头文件
-
初始化
string s1;//默认初始化,s1是一个空字符串
string s2=s1;//s2是s1的副本
string s3="hiya";//s3是该字符串字面值的副本
tring s4(10,'c');//s4的内容是cccccccccc
string s1默认初始化,s1是一个空串
string s2(s1)s2是s1的副本
string s2=s1等价于s2(s1),s2是s1的副本
string s3("value")s3是字面值"value”的副本,除了字面值最后的那个空字符外
string s3="value”等价于s3("value"),s3是字面值“value”的副本
string s4(n,'c')把s4初始化为由连续n个字符c组成的串
-
直接初始化和拷贝初始化
-
当初始值只有一个时,使用直接初始化或拷贝初始化都行。如果像上面的s4那样初始化要用到的值有多个,一般来说只能使用直接初始化的方式
string s5="hiya";//拷贝初始化
string s6("hiya");//直接初始化
string s7(10,'c');//直接初始化,s7的内容是cccccccccc
-
对于用多个值进行初始化的情况,非要用拷贝初始化的方式来处理也不是不可以,不过需要显式地创建一个(临时)对象用于拷贝
string s8=string(10,'c');//拷贝初始化,s8的内容是cccccccccc
-
std::cin的意思就是要使用命名空间std中的名字cin
-
using namespace::name;
using std::cin; cin>>i;//正确:cin和std::cin含义相同
-
位于头文件的代码一般来说不应该使用using声明。这是因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个using声明,那么每个使用了该头文件的文件就都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。
标准库类型string
-
使用string类型必须首先包含string头文件
-
初始化
string s1;//默认初始化,s1是一个空字符串
string s2=s1;//s2是s1的副本
string s3="hiya";//s3是该字符串字面值的副本
tring s4(10,'c');//s4的内容是cccccccccc
string s1默认初始化,s1是一个空串
string s2(s1)s2是s1的副本
string s2=s1等价于s2(s1),s2是s1的副本
string s3("value")s3是字面值"value”的副本,除了字面值最后的那个空字符外
string s3="value”等价于s3("value"),s3是字面值“value”的副本
string s4(n,'c')把s4初始化为由连续n个字符c组成的串
-
直接初始化和拷贝初始化
-
当初始值只有一个时,使用直接初始化或拷贝初始化都行。如果像上面的s4那样初始化要用到的值有多个,一般来说只能使用直接初始化的方式
string s5="hiya";//拷贝初始化
string s6("hiya");//直接初始化
string s7(10,'c');//直接初始化,s7的内容是cccccccccc
-
对于用多个值进行初始化的情况,非要用拷贝初始化的方式来处理也不是不可以,不过需要显式地创建一个(临时)对象用于拷贝
string s8=string(10,'c');//拷贝初始化,s8的内容是cccccccccc
使用string类型必须首先包含string头文件
初始化
string s1;//默认初始化,s1是一个空字符串 string s2=s1;//s2是s1的副本 string s3="hiya";//s3是该字符串字面值的副本 tring s4(10,'c');//s4的内容是cccccccccc
string s1默认初始化,s1是一个空串 string s2(s1)s2是s1的副本 string s2=s1等价于s2(s1),s2是s1的副本 string s3("value")s3是字面值"value”的副本,除了字面值最后的那个空字符外 string s3="value”等价于s3("value"),s3是字面值“value”的副本 string s4(n,'c')把s4初始化为由连续n个字符c组成的串
直接初始化和拷贝初始化
-
当初始值只有一个时,使用直接初始化或拷贝初始化都行。如果像上面的s4那样初始化要用到的值有多个,一般来说只能使用直接初始化的方式
string s5="hiya";//拷贝初始化 string s6("hiya");//直接初始化 string s7(10,'c');//直接初始化,s7的内容是cccccccccc
-
对于用多个值进行初始化的情况,非要用拷贝初始化的方式来处理也不是不可以,不过需要显式地创建一个(临时)对象用于拷贝
string s8=string(10,'c');//拷贝初始化,s8的内容是cccccccccc



