2.1 基本内置类型
内置类型包括算术类型和布尔类型。
1.算术类型分为两类:整型(integral type,包括字符和布尔类型在内)和浮点型。
布尔类型(bool)的取值是真(true)或者假(false)。
2.除去布尔型和扩展的字符型之外,其他整型可以划分为带符号的(signed)和无符号的(unsigned)两种。带符号类型可以表示正数、负数或0,无符号类型则仅能表示大于等于0的值。
类型int、short、long和long long都是带符号的,通过在这些类型名前添加unsigned就可以得到无符号类型,例如unsigned long。类型unsigned int可以缩写为unsigned。
与其他整型不同,字符型被分为了三种:char、signed char和unsigned char。特别需要注意的是:类型char和类型signed char并不一样。尽管字符型有三种,但是字符的表现形式却只有两种:带符号的和无符号的。类型char实际上会表现为上述两种形式中的一种,具体是哪种由编译器决定。
3.unsigned也能接收负值,不过会自动将这个负值转化为正值。signed 转 unsigned超出范围时会进行取模赋值,如将 -1 赋值给unsigned char类型的变量,则最后的赋值结果是:-1 % 2^sizeof(unsigned char)
例如,8比特大小的unsigned char可以表示0-255区间内的值,如果我们赋予了一个区间以外的值,则实际结果是该值对256取模后的余数,结果是255.
4.以0开头的数代表八进制,以0x开头的数表示十六进制。
5.由单引号括起来的一个字符称为char型字面值,双引号括起来的零个或多个字符则构成字符串型字面值。
6.转义字符
-
换行符 \n 横向制表符 \t 报警(响铃)符 \a 纵向制表符 \v 退格浮 \b 双引号 \" 反斜线 \\ 问号 \? 单引号 \' 回车符 \r 进纸符 \
如果反斜线\后面跟着的八位数字超过3个,只有前3个数字与\构成转义序列,例如\1234表示2个字符,但是\x要用到后面跟着的所有数字。
7.指定字面值的类型
8.字面值常量
一个形如42的值被称作字面值常量。每个字面值常量都对应一种数据类型,字面值常量的值和形式决定了它的类型。
2.2 变量
1.变量定义的基本形式是:首先是类型说明符(type specifier),随后紧跟由一个或多个变量名组成的列表,其中变量名以逗号分隔,最后以分号结束。列表中每个变量名的类型都由类型说明符指定,定义时还可以为一个或多个变量赋初值。变量定义的形式:类型 变量名1, 变量名2;
2.对象是指一块能存储数据并具有某种类型的内存空间。
3.初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。
4.作为C++11新标准的一部分,用花括号来初始化变量得到了全面应用,而在此之前,这种初始化的形式仅在某些受限的场合下才能使用。这种初始化的形式被称为列表初始化(list initialization)。现在,无论是初始化对象还是某些时候为对象赋新值,都可以使用这样一组由花括号括起来的初始值了。
(1)大括号初始化
int a=0,b={0},c(0),d{0}
- 1
对于b,d用{ }初始化,当初始值存在丢失精度风险时,编译器报错。
(2)默认初始化
定义于任何函数体之外的变量初始为0,函数体内值未定义。
5.
6.变量命名规范
变量命名有许多约定俗成的规范,下面的这些规范能有效提高程序的可读性:
- 标识符要能体现实际含义。
- 变量名一般用小写字母,如index,不要使用Index或INDEX。
- 用户自定义的类名一般以大写字母开头,如Sales_item。
- 如果标识符由多个单词组成,则单词间应有明显区分,如student_loan或studentLoan,不要使用studentloan。
- 变量名由字母,下划线,数字组成,以字母或下划线开头,不能连续两个下划线,不能下划线连大写字母开头,函数体外定义不能下划线开头。
7.变量定义时如果没有初始化,则会被默认初始:
- 函数体外的变量会被初始化为0
- 函数体内的未初始化的变量则不会被默认初始化
- 未被初始化的内置类型变量的值是未定义的
- 类的对象如果没有显示地初始化,则其值由类确定
- 如果试图拷贝或以其他形式访问未定义的值,则会引发错误
8.c++支持分离式编译机制,允许将程序分割为若干个文件,每个文件可被独立编译。为了支持这个机制,c++将声明和定义区分开来。
9.声明和定义
- 声明使名字为程序所知,一个文件如果想使用别处定义的名字,则必须包含对那个名字的声明。
- 声明规定了变量的类型和名字,这点和定义是相同的,但是仅声明一个变量的话需要在变量名前加上关键字extern,而且不要显示地初始化变量。
- 定义负责创建和名字关联的实体、还会申请存储空间,也可能会为变量赋初始值
- 变量只能被定义一次,但是可以被声明多次
- 任何包含了显示初始化的声明会变成定义,不管是否加了extern
- 在函数体内部初始化一个由extern标记了的变量将会引发错误 e.g.
-
extern int i; // 声明i而非定义 i int j; // 声明并定义 j
-
extern int i;//声明存在int,但不申请存储空间,可以多次重复声明。
extern int i=10;//函数体外,相当于定义;函数体内,引发错误。
10.作用域
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int reused = 42; // 全局作用域
int main()
{
// 块作用域,声明周期到main函数结束为止
int unique = 0;
// 使用的是全局作用域中的reused
cout << reused << " " << unique << endl;
// 定义一个块作用域内的变量reused,覆盖掉全局作用域中的同名变量
int reused = 0;
// 使用的是块作用域中的reused
cout << reused << " " << unique << endl;
// 显示地访问全局作用域中的reused,因为全局作用域本身没有名字,所以作用域操作符的左侧为空时,向全局作用域发起请求
cout << ::reused << " " << unique << endl;
return 0;
}
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int reused = 42; // 全局作用域
int main()
{
// 块作用域,声明周期到main函数结束为止
int unique = 0;
// 使用的是全局作用域中的reused
cout << reused << " " << unique << endl;
// 定义一个块作用域内的变量reused,覆盖掉全局作用域中的同名变量
int reused = 0;
// 使用的是块作用域中的reused
cout << reused << " " << unique << endl;
// 显示地访问全局作用域中的reused
cout << ::reused << " " << unique << endl;
return 0;
}
2.3 复合类型
1.引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。
int a=2; int &b=a;
- 引用不是对象,只是一个已经存在的对象的别名,所以不能定义引用的引用
- 引用必须被初始化,且初始值必须是一个对象(非字面值,常量引用可以使用字面值初始化),初始化之后无法被重新与其他的对象进行绑定 例如:int &a=10;//错误,引用类型的初始值必须是一个对象; double a=3.14;int &b=a;//错误,此处引用类型的初始值必须是int
- 给引用赋值实际上是给引用绑定的对象赋值 :double d=0,&r2=d;r2=3.1415;//正确,相当于把3.1415给了d。
- 一般情况下引用的类型需要与绑定的对象的类型一致,两种例外情况如下:
- 引用的类型与对象的类型存在继承关系:SubClass sub; Parent &p1 = sub;
- 引用的类型与对象的类型含有一个可接受的const类型转换规则:
/**
* 下面的代码编译器会将其优化为:
* double dval = 3.14;
* const int temp = dval;
* const int &ri = temp;
* 实际使用中避免这种情况。
*/
double dval = 3.14;
const int &ri = dval;
2.指针值
指针的值(即地址)应属下列4种状态之一:
1.指向一个对象。
2.指向紧邻对象所占空间的下一个位置。
3.空指针,意味着指针没有指向任何对象。
4.无效指针,也就是上述情况之外的其他值。
指针,建议初始化所有的指针
- 指针本身是一个对象,允许指针赋值和拷贝
- 指针不要求在定义时就初始化 //这两个是指针和引用的区别。
- 函数体内的指针未初始化时的值也是未定义的
- 指针存放的是指定对象的地址,获取对象的地址可以使用取地址符 &
- 一般情况下引用的类型需要与绑定的对象的类型一致,两种例外情况与引用的相同
- 无效指针(野指针):值是未定义的指针
- 空指针:没有指向任何对象的指针,生成空指针的方法:
int *p = nullptr;
int *p = 0;
// #include cstdlib
int *p = NULL;
3.当用到一个预处理变量时,预处理器会自动地将它替换为实际值,因此用NULL初始化指针和用0初始化指针是一样的。在新标准下,现在的C++程序最好使用nullptr,同时尽量避免使用NULL。
4.指针和引用都能提供对其他对象的间接访问,然而在具体实现细节上二者有很大不同,其中最重要的一点就是引用本身并非一个对象。一旦定义了引用,就无法令其再绑定到另外的对象,之后每次使用这个引用都是访问它最初绑定的那个对象。
5.void*是一种特殊的指针类型,可用于存放任意对象的地址。一个void*指针存放着一个地址,这一点和其他指针类似。
6.一般来说,声明符中修饰符的个数并没有限制。当有多个修饰符连写在一起时,按照其逻辑关系详加解释即可。以指针为例,指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针当中。
通过*的个数可以区分指针的级别。也就是说,**表示指向指针的指针,***表示指向指针的指针的指针,以此类推。
7.引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用。
8.通过解引用符 * 来利用指针访问对象,给解引用的结果赋值等价于给指针所指的对象赋值。
9.指向指针的引用:int *&r;
2.4 const限定符
1.const对象一旦创建后其值就不能再改变,所以const对象必须初始化。一如既往,初始值可以是任意复杂的表达式。
2.如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字。
//主文件
extern const int bufSize=512;//定义const
//其他文件
extern const int bufSize;//引用bufSize=512
3.指针本身是一个对象,它又可以指向另外一个对象。因此,指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题。用名词顶层const(top-level const)表示指针本身是个常量,而用名词底层const(low-level const)表示指针所指的对象是一个常量。
4.顶层const可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型、类、指针等。底层const则与指针和引用等复合类型的基本类型部分有关。比较特殊的是,指针类型既可以是顶层const也可以是底层const,这一点和其他类型相比区别明显。
注:把*放在const之前用以说明指针是一个常量,也就是说不变的是指针本身的值,并非指向的那个值。
int i = 0;
int *const p1 = &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资格,或者两个对象的数据类型必须能够转换,一般来说,非常量可以转换成常量,反之不行。
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上
5.常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式。
常量表达式要求:数据类型是常量,且初始值在编译过程中能得到计算结果。
6.C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。
constexpr int mf=20;
constexpr int limit=mf+1;
constexpr int sz=size();//当size()是一个constexpr函数时正确
7.常量引用可以指向常量引用与非常量引用,但是非常量引用不能指向常量引用,常量引用不能重新赋值,常量引用可以使用字面值进行初始化
常量指针可以指向常量对象与非常量对象,但是非常量指针不能指向常量对象,常量指针可以重新赋值,但是不能通过常量指针修改其指向的对象
常量指针必须初始化,比如:
int *const p1 = &i;
8.常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为"字面值类型"(literal type)。
算术类型、引用和指针都属于字面值类型。自定义类scales_item、IO库、string类型不属于字面值类型,不能被定义为constexpr。
一个constexpr指针的初始值必须是nullptr或者是0,或者是存储于某个固定地址中的对象。
constexpr既可以指向常量又可以指向非常量。
2.5 处理类型
1.类型别名(type alias)是一个名字,它是某种类型的同义词。使用类型别名有很多好处,它让复杂的类型名字变得简单明了、易于理解和使用,还有助于程序员清楚地知道使用该类型的真实目的。
有两种方法可用于定义类型别名。传统的方法是使用关键字typedef:
typedef double wages; //wages是double的同义词
typedef wages base, *p; //base是double的同义词,p是double*的同义词
新标准规定了一种新的方法,使用别名声明(alias declaration)来定义类型的别名:
using SI = Sales_item; // SI是Sales_item的同义词
2.auto类型
- auto i = 1; 编译器会自动分析推断 i 的类型
- auto定义的变量必须要初始化
- auto在一条语句上声明多个变量时,该语句的所有变量的初始基本数据类型都必须一样:auto i = 0, b = 1;
#include <iostream>
int main()
{
const int i = 1;
// j 是int,并不是const int
auto j = i;
// k 是const int
const auto k = j;
// m 是const int*,而不是int*
auto m = &i;
// n 是const int&,而不是int&
auto &n = i;
return 0;
}
①引用其实是使用引用的对象,当引用作为初始值时候,其实真正参与初始化的是引用对象的值,此时以引用对象的类型作为auto的类型:
int i=0,&r=i;
auto a=r;//a为int
②auto一般忽略顶层const,同时底层const会保留
const int ci=i,&cr=ci;
auto b=ci; //b为int(ci的顶层const被忽略)
auto c=cr; //c为int(cr是ci别名,ci是顶层const)
auto d=&i;//d为int*(int的地址就是指向int的指针)
auto e=&ci; //e为const int*(对常量取地址是一种底层const)
③若希望auto是顶层const,需明确指出:const auto f=ci; //c为int f为const Int
④auto &g=ci; //g为const int&
3.decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。
如果decltype使用的表达式是一个变量,则他返回变量的类型。如果decltype使用的表达式不是一个变量,则他返回表达式结果对应的类型。但是不实际计算该表达式。
//decltype完美复制了顶层const与引用
const int ci=0,&cj=ci;
decltype(ci) x=0;//const int x=0;
decltype(cj) y=x;//const int &y=x;
decltype(cj) z;//错误,引用未初始化
decltype(cj+0) z;//正确,等价于int z;
//对于" (变量名) "的形式
int i,j;
decltype((i)) d=j;//int &d=j;
赋值会产生引用,引用的类型就是左值的类型,也就是说如果i是int,则表达式i=x的类型是int&.
4.decltype((variable))(注意是双层括号)的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用。decltype((i)) d=j;//int &d=j;
2.6 自定义数据结构
1.我们的类以关键字struct开始,紧跟着类名和类体(其中类体部分可以为空)。类体由花括号包围形成了一个新的作用域)。类内部定义的名字必须唯一,但是可以与类外部定义的名字重复。
struct A {
// 类体
} variable1, variable2;
// 等价于:
struct A {
// 类体
};
A variable1, variable2;
c++11为类内成员进行默认初始化:struct edge{int u=10,v;}E;//u==10,v==0
2.确保头文件多次包含仍能安全工作的常用技术是预处理器(preprocessor),它由C++语言从C语言继承而来。预处理器是在编译之前执行的一段程序,可以部分地改变我们所写的程序。之前已经用到了一项预处理功能#include,当预处理器看到#include标记时就会用指定的头文件的内容代替#include。
3.C++程序还会用到的一项预处理功能是头文件保护符(header guard),头文件保护符依赖于预处理变量(参见2.3.2节,第53页)。预处理变量有两种状态:已定义和未定义。#define指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:#ifdef当且仅当变量已定义时为真,#ifndef当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到#endif指令为止。
文件头保护符:#ifndef-#endif,#ifdef-#endif,无视作用域的规则,必须唯一
#define DEBUG
//将DEBUG设定为预处理变量,至#undef 或整个程序结束,无视作用域规则
#ifdef DEBUG
//DEBUG为预处理变量时为真,执行语句至#endif
#endif
#undef DEBUG
//释放DEBUG定义
#ifndef DEBUG
//DEBUG为非预处理变量时为真,执行语句至#endif
#endif