一、基本内置类型
C++基本内置类型包括算术类型和空类型。
1.算术类型
算术类型分为两类:整形(包括字符和布尔型在内)和浮点型
bool(布尔型) 长度(字节):1 取值范围:false,true
char(字符型) 长度(字节):1 取值范围:-128~127
signed char(有符号字符型) 长度(字节):1 取值范围:-128~127
unsigned char(无符号字符型)长度(字节):1 取值范围:0~255
short(signed short) 长度(字节):2 取值范围:-32768~32767
unsigned short 长度(字节):2 取值范围:0~65535
int(signed int) 长度(字节):4 取值范围:-2147483648~2147483647
unsigned int 长度(字节):4 取值范围:0~4294967295
long(signed long) 长度(字节):4 取值范围:-2147483648~2147483647
unsigned long 长度(字节):4 取值范围:0~4294967295
float(浮点型,表示实数) 长度(字节):4 取值范围:3.4*10^-38~3.4*10^38
double(双精度浮点型) 长度(字节):8 取值范围:1.7*10^-308~1.7*10^308
long double 长度(字节):8 取值范围:1.7*10^-308~1.7*10^308
与整型不同,字符型被分为了三种:char、signed char、unsigned char。尽管字符型有三种,但是字符的表现形式却只有两种:带符号的和无符号的。类型char实际上会表现为上述两种形式的一种,具体是哪种由编译器决定。
2.类型转换
bool b=42; //b为true
int i=b; //i的值为1
i=3.14; //i的值为
double pi=i; //pi=3.0
unsigned char c=-1; //假设c为8比特,c=256
signed char c2=256; //c2的值未定义
当我们赋予无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。
当我们赋予带符号类型一个超出它表示范围的值时,结果是未定义的。
unsigned u=10;
int i=-42;
std::cout<<i+i<<std::endl; //输出-84
std::cout<<u+i<<std::endl; //如果int占32位,输出4294967264(i等于2的32次方减42)
如果表达式里既有带符号类型又有无符号类型,当带符号类型取值为负时会出现异常,这是因为带符号数会自动地转换为无符号数。
把负数转换为无符号数类似于直接给无符号数赋一个负值,结果等于这个负值加上无符号数的模。
3.字面值常量
一个形如42的值被称为字面值常量,这样的值一望而知。每个字面值都对应一种字符类型,字面值常量的形式和值决定了它的数据类型。
整形和浮点型字面值:
以0开头的代表八进制数,以0x或0X开头的代表十六进制——20/*十进制*/ 024/*八进制*/ 0x14/*十六进制*/
十进制字面值的类型是int、long、long long中尺寸最小的那个,当然前提是这种类型要能容纳下当前的值。short没有对应的字面值。
尽管整形字面值可以存储在带符号数据类型中,但严格来说,十进制字面值不会是负数。例如-42,那个负号并不在字面值之内,它的作用仅仅是对字面值取负值而已。
默认的,浮点型字面值是一个double。
字符和字符串字面值:
'a' //字符字面值
"hello world" //字符串字面值
字符串字面值的实际类型是由常量字符构成的数组,编译器在每个字符串的结尾处添加一个空字符(‘\0’),因此,字符串字面值的实际长度要比它的内容多1。
转义序列
换行符 \n | 横向制表符 \t | 报警(响铃)符 \a |
纵向制表符 \v | 退格符 \b | 双引号 \" |
反斜符 \\ | 问号 \? | 单引号 \' |
回车符 \r | 进纸符 \f |
在程序中,上述转义序列被当做一个字符使用。
指定字面值的类型:
前缀 | 含义 | 类型 |
---|---|---|
u | Unicode16字符 | char16_t |
U | Unicode32字符 | char32_t |
L | 宽字符 | wchar_t |
u8 | UTF-8(仅用于字符串字面值常量) | char |
后缀 | 类型 |
u or U | unsigned |
l or L | long |
ll or LL | long long |
f or F | float |
l or L(浮点型) | long double |
布尔型和指针型字面值:
true和false是布尔型的字面值。
nullptr是指针字面值。
二、变量
变量提供一个具名的、可供程序操作的存储空间。
对C++程序员来说,“变量”和“对象”一般可以互换使用。
1.变量定义
变量的定义并初始化:
double price=109.99,discount=peice*0.16;
double salePrice=applyDiscount(price,discount);
特别注意:初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而已一个新值来代替。
列表初始化:
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;//正确:转换执行,且确实丢失了部分值
默认初始化:
1.定于任何函数体之外的变量都被初始化0;
2.定义于函数体内部的内置类型的对象如果没有初始化,则其值未定义。
3.类的对象如果没有显式的初始化,则其值由类确定
提示:未初始化变量可能引发运行时故障,建议初始化每一个内置类型的变量。虽然并非必须这么做,但如果我们不能确保初始化后程序安全,那么这么做不失为一种简单可靠的方法。
变量声明和定义的关系:
如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显示地初始化变量:
extern int i;//声明i
int j;//声明并定义j
任何包含了显示初始化的声明即成为定义:
extern double pi=3.1416;//定义
变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。
3.标识符(命名规则)
①标识符由字母、数字和下划线组成,其中必须以字母或下划线开头
②一些关键字和操作符替代名不能用作标识符
③用户自定义的标识符中不能连续出现两个下划线,也不饿能以下划线紧连着大写字母开头
4.名字的作用域
作用域都以花括号{ }分割。
全局作用域、块作用域
嵌套的作用域:内层作用域、外层作用域
作用域中一旦声明了某个名字,它所嵌套着的所有作用域中都能访问该名字。同时,允许在内层作用域中重新定义外层作用域已有的名字:
#include <iostream>
int reused=42;
int main()
{
int unique=0;
std::cout<<reused<<" "<<unique<<std::endl;//输出42 0
int reused=0;
std::cout<<reused<<" "<<unique<<std::endl;//覆盖全局变量,输出0 0
std::cout<<::reused<<" "<<unique<<std::endl;//显式地访问全局变量reused,输出 42 0
return 0;
}
注:如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量。
三、复合类型
复合类型是基于其他类型定义的类型。
1.引用
通过将声明符写成&d的形式来定义引用类型,其中d为声明的变量名。
int ival=1024;
int &refVal=ival; //reval指向ival(是ival的另一个名字)
int &refVal2; //错误:引用必须被初始化
定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定在另外一个对象,因此引用必须初始化。
引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。对引用的操作本质上都是对引用绑定的对象操作。
因为引用本身不是一个对象,所以不能定义引用的引用。
引用的定义:
①允许在一条语句中定义多个引用,其中每个引用标识符都必须以符号&开头;
②一般的,所有引用的类型都要和与之绑定的对象严格匹配;
③引用只能绑定到对象上,而不能与字面值或某个表达式的计算结果绑定在一起
int &refVal4=10; //错误,引用类型的初始值必须是一个对象
double dval=3.14;
int &refVal5=dval;//错误,此处引用类型的初始值必须是int型对象
2、指针
指针与引用都是符合类型,实现了对其他对象的间接访问。
两者不同点:
①指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的声明周期内它可以先后指向几个不同的对象。
②指针无需在定义时赋初值
int *ip1,*ip2; //ip1和ip2都是int型指针
double dp,*dp2; //dp是double型对象,dp2是double型指针
指针存放某个对象的地址,想要获取该地址,需要使用取地址符(操作符&):
int ival=42;
int *p=&ival;
因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。
所有指针的类型都要和它所指向的对象严格匹配:
double dval;
double *pd=&dval;
double *pd2=pd;
int *pi=pd; //错误
pi=&dval; //错误
如果指针指向了一个对象,则允许使用解引用符(操作符*)来访问该对象。
解引用操作仅适用于那些确实指向了某个对象的有效指针。
空指针:三种生成空指针的方法
int *p1=nullptr;
int *p2=0;
//需要首先#include cstdlib
int *p3=NULL;
把int变量直接赋给指针是错误的操作,即使int变量的值恰好等于0也不行:
int zero=0;
pi=zero; //错误,不能把int变量直接赋给指针
指针的赋值:
pi=&ival; //改变了指针本身的值
*pi=0; //改变了指针所指对象的值
指针与布尔型:
任何非0指针对应的条件值都是true。
void*指针:
void*是一种特殊的指针类型,可用于存放任意对象的地址。
利用void*指针能做的事情比较有限;拿它和别的指针比较、作为函数的输入与输出、或者赋给另外一个void*指针。不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。
3.复合变量的声明
一条定义语句可能定义出不同类型的变量:
int i=1024,*p=&i,&r=i;
基本数据类型是int而非int*。*仅仅是修饰了p而已,对该声明语句中的其他变量,它并不产生任何作用。
指向指针的指针:
**表示指向指针的指针,***表示指向指针的指针的指针:
int ival=1024;
int *pi=&ival;
int **ppi=π
指向指针的引用:
引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用:
int i=42;
int *p;
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引用的是一个int指针。
四、const限定符
const将变量限定成常量,其值不可改变。
因为const对象一旦创建后其值就不能再改变,所以const对象必须初始化。
只能在const类型的对象上执行不改变其内容的操作
编译器将在编译过程中把用到该变量的地方都替换成对应的值。
默认情况下,const对象被设定成仅在文件内有效,当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别设定了独立的变量。
extern const int bufSize=fcn(); //file_1.cc定义并初始化了一个常量
extern const int bufSize; //与file_1.cc中定义的bufSize是同一个
1.const的引用
对const常量对象的引用必须是const常量,且与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象:
const int ci=1024;
const int &r1=ci; //正确:引用及其对应的对象都是常量
r1=42; //错误:r1是对常量的引用,不能修改绑定的对象
int &r2=ci; //错误:试图让一个非常量引用指向一个常量对象
初始化常量时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式:
int i=42;
const int &r1=i; //正确
const int &r2=42; //正确
const int &r3=r1*2; //正确
int &r4=r1*2; //错误,r4是一个普通的非常量引用
const的引用可能引用一个非const的对象,但是不允许通过引用改变所绑定对象的值。
int i=42;
int &r1=i;
const int &r2=i;
r1=0;
r2=0; //错误
2、指针与const
以下几点与常量引用类似:
①想要存放常量对象的地址,只能使用指向常量的指针
②允许令一个指向常量的指针指向一个非常量对象,但该指针不能用于改变其所指对象的值。
const指针:
指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定位常量。常量指针必须初始化,而且一旦初始化完成,它的值(存放在指针中的那个地址)就不能再改变了。把*放在const关键字之前用以说明指针是一个常量,这样书写隐含着一层意味,即不变的是指针本身而非指向的那个值:
int erroNumb=0;
int *const curErr=&errNumb;
const double pi=3.1415926;
const double *const pip=π //pip是一个指向常量对象的常量指针
指针本身是一个常量并不意味着不能通过指针修改其所指对象的值,能否这样做完全依赖于所指对象的类型。例如,pip是一个指向常量的常量指针,则不论是pip所指的对象值还是pip自己存储的那个地址都不能改变。相反的,curErr指向的是一个一般的非常量指针,那么就完全可以用currErr去修改errNumb的值。
总结:
①指向常量的指针或常量引用前必须加const;
②const引用绑定的对象、指向常量的指针的对象也可以非常量对象,但不允许通过其改变所指对象的值;
③允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式,而非常量引用只能绑定到对象上,而不能与字面值或某个表达式的计算结果绑定在一起
④常量指针也可以指向非常量,不变的是指针本身的值而不是指向的那个值
⑤常量指针和常量不能被赋值,只能初始化
3.顶层const
顶层const表示对象本身是个常量,底层const表示指针或引用所指的是个常量。用于声明引用的const都是底层const
int i=0;
int *const p1=&i; //顶层const
const int ci=42; //顶层const
const int *p2=&ci; //允许改变p2的值,底层const
const int *const p3=p2; //p3既是顶层const(右半部分),又是底层const(左半部分)
const int &r=ci; //用于声明引用的const都是底层const
当拷贝对象时,顶层const不受影响。另一方面,底层const的限制却不能忽略。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换为常量,反之则不行。
i=ci; //正确,ci是一个顶层const,对此操作无影响
p2=p3; //正确,p2和p3指向的对象类型相同,p3顶层const的部分不影响
int *p=p3; //错误:p3包含底层const的定义,而p没有
p2=p3; //正确:两者都是底层const
p2=&i; //正确:int *能转换成const int*
int &r=ci; //错误:普通的int&不能绑定到int常量上
const int &r2=i; //正确:const int&可以绑定到一个普通int上
4.常量表达式
常量表达式是指值不会改变并且在编译过程中就能得到计算结果的表达式。
显然,字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。
一般来说,如果认定变量是一个常量表达式,那就把它生命成constexpr类型。声明成constexpr的变量一定是一个常量,而且必须用常量表达式初始化。
字面值类型:
类型一般比较简单,值也显而易见、容易得到。
算术类型、引用和指针都属于字面值类型。
指针和constexpr:
在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关(constexpr把它所定义的对象置为了顶层const):
const int *p=nullptr; //p是一个指向整型常量的指针
constexpr int *q=nullptr; //q是一个指向整数的常量指针
与其他常量指针类似,constexpr指针既可以指向常量也可以指向一个非常量。
五、处理类型
1.类型别名
类型别名是一个名字,它是某种类型的同义词。
两种方法定义:
typedef double wages;
typedef wages base,*p; //base是double的同义词,p是double*的同义词
using SI=Sales_items;
指针、常量和类型别名:
typedef char *pstring;
const pstring cstr=0; //cstr是指向char的常量指针
const pstring *ps; //ps是一个指针,它的对象是指向char的常量指针
pstring实际上是指向char的指针,因此,const pstring就是指向char的常量指针,而非指向常量字符的指针。
2.auto类型说明符
auto能让编译器替我们去分析表达式所属的类型。
使用auto也能在一条表达式中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样。
复合类型、常量和auto:
当引用被用作初始值时,真正参与初始化的其实是引用对象的值,此时编译器以引用对象的类型作为auto的类型:
int i=0,&r=i;
auto a=r; //a是一个整数
其次,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*类型
auto e=&ci; //e为const int*类型(对常量对象取地址是一种底层const)
如果希望推断出的auto类型是一个顶层const,需要明确指出:
const auto f=ci;
设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留。和往常一样,如果我们给初始值绑定一个引用,此时的常量就不再是顶层常量了。
auto &g=ci; //g是一个整形常量引用,绑定到ci
auto &h=42; //错误:不能为非常量引用绑定字面值
const auto &j=42; //正确:可以为常量引用绑定字面值
要在一条语句中定义多个变量,符号&和*只从属于某个声明符,而非基本数据类型的一部分,因此初始值必须是同一种类型。
3.decltype类型指示符
decltype的作用是选择并返回操作数的数据类型。
如果decltype使用的表达式是一个变量,则decltype返回该变量的类型,包括顶层const和引用在内:
const int ci=0,&cj=ci;
decltype(ci) x=0; //x的类型为const int
decltype(cj) y=0; //y的类型为const int&,y绑定到变量x
decltype(cj) z; //错误:z是一个引用,必须初始化
decltype和引用:
int i=42,*p=&i,&r=i;
decltype(r+0) b; //正确:加法的结果是int而非int&,因此b是一个int
decltype (*p) c; //错误:c是int&,必须初始化
decltype((i)) d; //错误:d是int&,必须初始化
decltype(i) e; //正确:e是一个未初始化的int
当引用是表达式的一部分,decltype这个表达的结果是一个具体值而非一个引用。
如果表达式的内容是解引用操作,则decltype将得到引用类型。
decltype((variable))(注意是双层括号)的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用。
六、自定义数据结构
数据结构是把一组相关的数据元素组织起来然后使用它们的策略和方法,C++允许用户以类的形式自定义数据结构。
定义Sales_data类:
struct Sales_data{/*...*/};
Sales_data accum,trans;
类定义的最后有分号,不要忘记加。
最好不要把对象的定义和类的定义放在一起。
类的数据成员:
类的数据成员定义了类的对象的具体内容,每个对象有自己的一份数据成员拷贝,修改一个对象的数据成员,不会影响其他Sales_data的对象。
定义数据成员的方法和定义普通变量一样。
新标准规定,可以为数据成员提供一个类内初始值。创建对象时,类内初始值将用作初始化数据成员。没有初始值的成员将被默认初始化。
头文件:
为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应该与类的名字一样。
头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明。
预处理器:
#include:当预处理器看到#include标记时就会用指定的头文件的内容代替#include
头文件保护符:
#define指令把一个名字设定成预处理变量,#ifdef当且仅当变量已定义时为真,#ifndef当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到#endif指令为止。
预处理变量无视C++语言中关于作用域的规则。
为了避免与程序中的其他实体发生名字冲突,一般把预处理变量的名字全部大写。