该笔记包括内置类型的介绍、变量的声明和定义、引用、指针、const
、类型别名(含auto
和decltype
)和自定义数据结构struct
,还有头文件保护的方法。
目录
内置类型
算术类型的选择
- 当明确知晓数值不可能为负时,选用无符号类型
unsigned
。- 含有无符号类型的表达式:
- 当一个算术表达式既有无符号数又有
int
值时,那个int
值会转化为无符号数。 - 当从无符号数减去一个值时,不论这个值是否是无符号数,我们都必须确保结果不是一个负值(也不可能得到负值)。
- 当一个算术表达式既有无符号数又有
- 含有无符号类型的表达式:
- 使用
int
执行整数运算。在实际应用中,short
常常显得太小而long
一般与int
相同,如果数值超过int
的范围,选用long long
。 - 在算术表达式中,不要使用
char
或bool
。 - 执行浮点数运算选用
double
。float
通常精度不够且计算代价相差无几,long double
提供的精度在一般情况下没有必要。
字符和字符串字面值:
'a' // 字符字面值 "Hello World!" // 字符串字面值
字符串字面值的类型实际上是由常量字符构成的数组。编译器在每个字符串的结尾处添加一个空字符'\0'
,因此字符串字面值的实际长度比它的内容多1。例如,字符串"A"
代表了一个字符的数组,包含两个字符,一个是字母A、另一个是空字符。
如果两个字符串字面值紧邻且仅有空格、缩进和换行符分割,则它们其实是一个整体。可以将一段较长的字符串分开写:
std::cout << "a really, really long string literal " "that spans two lines" << std::endl;
变量
初始化
初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值替代。
列表初始化:(C++11新标准)
int units_sold = 0; int units_sold = {0}; int units_sold{0}; int units_sold(0);
当使用内置类型变量时,这种初始化形式需要注意:如果使用列表初始化且初始值存在丢失信息的风险,则编译器奖报错:
long double ld = 3.1415926536; int a{ld}, b = {ld}; //错误:转换未执行,因为存在丢失信息的危险。 int c = ld, d = ld; //正确:转换执行,且确实丢失了部分值。
默认初始化:
如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。定义于任何函数体之外的变量被初始化为0;定义在函数体内部的内置类型变量将不被初始化。一个未被初始化的内置类型变量的值是未定义的,如果试图拷贝或以其他形式访问此类值将引发错误。
变量声明和定义的关系
C++语言支持分离式编译机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。
为了支持分离式编译,C++语言将声明和定义区分开。声明(declaration)使得名字为程序所知,一个文件如果想要使用别处定义的名字则必须包含对那个名字的声明;而定义(definition)负责创建与名字关联的实体。
变量声明规定了变量的类型和名字,在这一点上定义与之相同。但除此之外,定义还申请了存储空间,也可能为变量赋一个初始值。
如果像声明一个变量并定义它,就在变量名前添加关键字extern
,而且不要显式地初始化变量:
extern int i; //声明i而非定义i int j; //声明并定义j extern double pi = 3.14 //定义
Note:变量能且只能被定义一次,但可以被多次声明。
Warning:如果要在多个文件中使用同一个变量,就必须将声明和定义分离。此时,变量的定义必须出现且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,绝不能重复定义。
变量命名规范
- 标识符要能体现实际含义。
- 变量名一般用小写字母,如index,不要使用Index或INDEX。
- 用户自定义的类名一般以大写字母开头,如Sales_item。
- 如果标识符由多个单词组成,则单词间应有明显区分,如student_loan或studentLoan,不要使用studentloan。
名字的作用域
使用全局变量需要在变量名前面加上作用域操作符::
。
复合类型
引用
引用(reference)为对象起了另外的一个名字,引用类型引用(refers to)另外一种类型。通过将声明符写成&d
的形式来定义引用类型,其中d
是声明的变量名。
引用即别名:引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。定义了一个引用后,对其进行的所有操作都是在与之绑定的对象上进行的。
int ival = 1024; int &refVal = ival; //refval指向ival(是ival的另一个名字) int &refVal2; //报错:引用必须被初始化 refVal = 2; //把2赋给refVal指向的对象,此处即是赋给了ival int ii = refVal; //与ii = ival执行结果一致 // 正确:refVal3绑定到里那个与refVal绑定的对象上,这里就是赋给了ival int &refVal3 = refVal; // 利用refVal绑定的对象的值初始化变量i int i = refVal;
指针
指针(pointer)是“指向(point to)”另一种类型的复合类型。
与引用的相同点:指针也实现了对其他对象的间接访问。
与引用的不同点:
- 其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。
- 其二,指针无需在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
指针的定义:定义指针类型的方法将声明符写成*d
的形式,其中d
是变量名。
int *ip1, *ip2; //ip1和ip2都是指向int型对象的指针 double dp, *dp2; //dp2是指向double型对象的指针,dp是double型对象。
获取对象的地址:指针存放某个对象的地址,想要获取该地址,需要使用取地址符&
。
int ival = 42; int *p = &ival; //p存放变量ival的地址,或者说p是指向变量ival的指针 double dval; double *pd = &dval; //正确:初始值是double型对象的地址 double *pd2 = pd; //正确:初始值是指向double对象的指针 int *pi = pd; //错误:指针pi的类型和pd的类型不匹配 pi = &dval; //错误:试图把double型对象的地址赋给int型指针
利用指针访问对象:如果指针指向了一个对象,则允许使用解引用符*
来访问该对象。
int ival = 42; int *p = &ival; //p存放着变量ival的地址 cout << *p; //由符号*得到指针p所指的对象,输出42 *p = 0; //由符号*得到指针p所指的对象,即可经由p为变量ival赋值 cout << *p; //输出0
空指针:空指针不指向任何对象。
生成空指针:
int *p1 = nullptr; //等价于 int *p = 0; int *p2 = 0; //直接将p2初始化为字面常量 //需要首先#include cstdlib int *p3 = NULL; //等价于int *p3 = 0;
指针的赋值
int i = 42; int *pi = 0; //pi被初始化,但没有指向任何对象 int *pi2 = &i; //pi2被初始化,存有i的地址 int *pi3; //如果pi3定义于块内,则pi3的值是无法确定的 pi3 = pi2; //pi3和pi2指向同一个对象i pi2 = 0; //现在pi2不指向任何对象了
可以记住赋值永远改变的是等号左边的对象,例如:
pi = &ival; //pi的值被改变,现在pi指向了ival *pi = 0; //ival的值被改变,指针pi并没有改变,即依然指向的是原先的地址
指向指针的指针
一般来说,声明符中修饰符的个数并没有限制。当有多个修饰符连写在一起时,按照其逻辑关系详加解释即可。例如,通过*
的个数可以区分指针的基本:**
代表指向指针的指针,***
代表指向指针的指针的指针。
int ival = 1024; int *pi = &ival; //pi指向一个int型的数 int **ppi = π //ppi指向一个int型的指针 //解引用指向指针的指针会得到一个指针。为了访问最原始的那个值,需要对指针做两次解引用。 cout << "The value of ival\n" << "direct value: " << ival << "\n" <<"indirect value: " << *pi << "\n" <<"doubly indirect value: " << **ppi << endl;
指向指针的引用
引用本身不是一个对象,使用不能定义指向引用的指针。但指针是对象,所以存在对指针的引用:
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
指针。
Note from Primer:面对一条比较复杂的指针或引用的声明语句时,从右往左阅读有助于弄清楚它的真实含义。
const限定符
定义常量,任何对其赋值的行为都将发生错误。
const int bufSize = 512; //输入缓冲区大小 bufSize = 512; //错误
默认状态下,const
对象仅在文件内有效。
但某些时候const
变量的初始值不是一个常量表达式,但又确实有必要在文件间共享。可以在一个文件中定义const
,而在其他多个文件中声明并使用它。
//file_1.cc定义并初始化了一个常量,该常量能被其他文件访问 extern const int bufSize = fsn(); //file_1.h头文件 extern const int bufSize; //与file_1.cc中定义的bufSize是同一个
const
的引用
与普通引用的区别在于,对常量的引用不能被用作修改它所绑定的对象。
const int ci = 1024; const int &r1 = ci; //正确:引用及其对应的对象都是常量 r1 = 42; //错误:r1是对常量的引用 int &r2 = c1; //错误:试图让一个非常量引用指针指向一个常量对象
初始化和对const
的引用
int i = 42; const int &r1 = i; //允许将const int &绑定到一个普通int对象上 const int &r2 = 42; //正确:r1是一个常量引用 const int &r3 = r1 * 2; //正确:r3是一个常量引用 int &r4 = r1 * 2; //错误:r4是一个普通的非常量引用
对const
的引用可能引用一个并非const
的对象
int i = 42; int& r1 = i; const int& r2 = i; r1 = 0; std::cout << i; //输出0 std::cout << r2; //输出0 r2 = 0; //错误
r2
绑定整数i
是合法的行为。然而,不允许通过r2
修改i
的值。但i
的值仍可以通过其他方式修改。
指针和const
指向常量的指针
指向常量的指针不能用于改变所指对象的值。想要存放常量对象的地址,只能使用指向常量的指针。
const double pi = 3.14; double *ptr = π //错误:ptr是一个普通指针 const double *cptr = π //正确:cptr可以指向一个双精度常量 *cptr = 42; //错误:不能给*cptr赋值
允许一个指向常量的指针指向一个非常量对象:
double dval = 3.14; cptr = &dval; //正确:但是不能通过cptr改变dval的值
Note from Primer:所谓指向常量的指针或引用,不过是指针或引用“自以为是”罢了,它们觉得自己指向了常量,所以自觉地不去改变所指对象的值。
const
指针
常量指针必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。
int errNumb = 0; int *const curErr = &errNumb; //curErr将一直指向errNumb const double pi = 3.14159; const double *const pip = π //pip是一个指向常量对象的常量指针
可以通过curErr
改变errNumb
的值,但不能通过pip
改变pi
的值。
区分顶层const底层const
顶层const表示指针本身是个常量;底层const表示指针所指的对象是个常量。更一般的,顶层const可以表示任意的对象是常量,这一点对任何数据类型都适用。
int i = 0; int *const p1 = &i; //不能改变p1的值,这是一个顶层const const int ci = 42; //不能改变ci的值,这是一个顶层const const int *p2 = &c1; //允许改变p2的值,这是一个底层const const int *const p3 = p2; //靠右的const是顶层const,靠左的是底层const const int &r = ci; //用于声明引用的const都是底层const
区分的作用:
- 执行对象拷贝时有限制。拷入拷出的对象必须具有相同的底层const,或者这两者的数据类型必须能够相互转换。
i = ci; //正确:拷贝ci的值,ci是一个顶层const,对此操作无影响 p2 = p3; //正确:p2和p3指向的对象类型相同,p3顶层const的部分不影响 int *p = p3; //错误:p3包含底层const的定义,而p没有 p2 = &i; //正确:int*能转换成const int* int &r = ci; //错误:普通的int&不能绑定到int常量上 const int &r2 = i; //正确:const int&可以绑定到普通int上
Note:这个概念看起来复杂,但实际上好像没什么意思?只要理解了指向常量的指针和常量指针的区别,上面的区分似乎无所谓。。。
处理类型
类型别名
typedef double wages; //wages是double的同义词 typedef wages base, *p; //base是double的同义词,p是double*的同义词 //新标准可以采用别名声明 using SI = Sales_item; //SI是Sales_item的同义词 wages hourly, weekly; SI item;
指针、常量和类型别名
typedef char *pstring; const pstring cstr = 0; //cstr是指向char的常量指针 const pstring *ps; //ps是一个指针,它的对象是指向char的常量指针
区分:
const pstring cstr = 0; //cstr是指向char的常量指针 const char *cstr = 0; //cstr是指向const char的指针
auto
类型说明符和 decltype
类型指示符简介(C++11新标准)
C++11新标准引入auto
说明符,可以让编译器替我们去分析表达式所属的类型。
//由val1和val2相加的结果可以推断出item的类型 auto item = val1 +val2;
C++11新标准引入decltype
,作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。
decltype(f()) sum = x; //sum的类型就是函数f的返回类型
自定义数据结构
struct Sales_data { std::string bookNo; unsigned units_sold = 0; double revenue = 0.0; }; //定义 Sales_data data1, data2; //访问和输入 double price = 0; std::cin >> data1.bookNo >> data1.units_sold >> price; data1.revenue = data1_units_sold * price;
头文件保护
头文件保护符:
#define
指令把一个名字设定为预处理变量。#ifdef
当且仅当变量已经定义时为真。#ifndef
当且仅当变量未定义时为真。- 一旦检查为真,将执行后续操作至
#endif
。
例如在Sale_data.h中:
#ifndef SALES_DATA_H #define SALES_DATA_H #include <string> struct Sales_data { std::string bookNo; unsigned units_sold = 0; double revenue = 0.0; }; #endif