目录
在创建对象时获得一个特定的値叫这个对象被初始化初始化不是赋值,初始化是创建变量时赋予一个初始值,而赋值是把对象当前値擦除以一个新値代替。
基本内置类型
算术类型:整型(包括字符型和布尔类型)和浮点型。
算术类型尺寸表详情见书P30
布尔类型的取值是真(ture)和假(false)。
一个基本字符类型char大小和一个机器字节序一样。
其他字符类型用于扩展字符集,类型char16_t和char32_t则为Unicode字符集服务(Unicode用于表示所有自然语言中字符的标准)
带符号类型和无符号类型
除了布尔型和扩展字符集外其他整型可划为带符号(signed)和无符号(unsigned)
和其他整型不同char被分成:char, signed char, unsigned char三种
注意类型char和类型signed char不一样,但是字符表现形式只有无符号和有符号,类型char是上述两种形式的哪种具体由编译器决定。
8比特的unsigned char表示0~255区间的値,8比特的signed char理论上表示-127~127但大多数计算机实际范围定为-128~127。
类型转换
将一种类型转换为另一种类型的运算
bool b=42; //b为真打印1
int i=b; //i値为1
i=3.14; //i的値为3,浮点数赋给整型做了近似处理,结果仅保留小数点前的部分
double pi=i //pi値为3.0,浮点数被赋给整型时精度丢失小数部分记为0
unsigned char c=-1;
10000000 00000000 00000000 00000001 原码
11111111 11111111 11111111 11111110 反码+1得补码
11111111 11111111 11111111 11111111 补码
11111111 取后八位因为一个字节占八个比特位因为是无符号所以整型提升高位补0
00000000 00000000 00000000 11111111
//所以c値为255
signed char c2=256; //赋给带符号类型一个超出范围的値结果是未定义的,程序可能报错/崩溃/产生垃圾数据
int i=42;
if(i) //true
{
i=0; //如果我们用非布尔的値作条件它就会自动转化为布尔值,这就是类型转换
}
//但布尔值非 0 即 1 ,所以不宜在算术转换中用布尔值
含有无符号类型的表达式
不要故意给无符号类型赋负值啊喂
当一个表达式既有无符号又有int値的话,那么那个int値会被转换为无符号数
unsigned u=10;
int i=-42;
std::cout<<i+i<<std::endl; //-42+-42输出-84
std::cout<<u+i<<std::endl; //如果int占32位,输出4294967264
-42 整型提升变为无符号数且int4字节32比特位
10000000 00000000 00000000 00101010 原码
11111111 11111111 11111111 11010101 反码+1得补码
11111111 11111111 11111111 11010110补码算出补码打印的値+10
得4294967040+214=4294967254+10所以打印4294967264
所以无符号数永远不会小于0 ,循环也如此
for (unsigned u=10;u>=0;u--)
//循环不会陷入死循环,因为u永远不小于0,循环成立 u=0减一时输出4294967295
for循环先执行后打印
可以换成while循环先判断后循环这样最后一次迭代输出0
字面值常量
像42这样的一看就知道是个字面值常量一望便知,字面值常量形式决定了它数据类型比如char int double
整型和浮点型字面值
我们可以将字面值写作十,八,十六进制
以0开头整数表示八进制
以0x和0X开头的表示十六进制
20/* 十进制*/ 024/*八进制*/ 0x14/*十六进制*/
默认情况下十进制是带符号数,八和十六既可能带符号也可能不带,尽管整型字面值可以储存在带符号数据类型里,但严格来说十进制字面值不会是负数,比如-42的负十进制字面值,符号不在字面值内仅仅只是对字面值取负值。
浮点型字面值表现为小数或者科学计数法表示的指数E/e
字符和字符串字面值
由单引号括起来的字符是char型字面值,双引号括起来的零个或多个字符则是字符串型字面值
' a ' //字符字面值 " hi " //字符串字面值
字符串型字面值类型实际上是由常量字符构成的数组,且编译器在每个字符串后面都加上\0这个空字符
如果两个字符串仅由空格,缩进或换行符分隔,它们实际上是一个整体
转义序列(均以反斜线开始\)
有两类字符不能直接使用,一种是不可打印的字符,比如退格等因为没有可视图符。另一种是有特殊含义的字符(单双引号,问号反斜线等)
转义字符有哪些
我们可以使用泛化的转义序列比如\x后面跟1到3个八进制或者1到多个十六进制数字,数字表示字符对应的数值,假设使用的是Latin-1字符集就有
\7 响铃 \12 换行 \40 空格
\0 空字符 \115 字符M \x4d 字符M
注意char字符类型在大多数机器上是占8比特位,十六进制字符占16位比如\x1234占4*8=32位可能报错?
所以超过八位的十六进制字符都是和某个前缀作为开头的扩展字符集一起使用
指定字面值的类型
我们可以用表中前缀或后缀改变整型,浮点型和字符型字面值默认类型
前缀 | 含义 | 类型 |
u | Unicode 16 字符 | char16_t |
U | Unicode 32 字符 | 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 和 fales是布尔类型的字面值,nullptr是指针字面值。
boll test = fales;
变量
int sum=0, value , //sum和value和units_sold都是int
units_sold=0; //sum和units_sold初始值都是0
Sales_item item; //item类型是Sales_item
std::string book("0-201-78345-X"); //book通过string字面值初始化
//string是一种库类型表示一个可以变长的字符序列
注意,何为对象?
通常来说对象是指一块能存储数据并具有某种类型的内存空间
在使用对象这个词时并不严格区分是类还是内置类型,也不区分是否命名和只读
初始值
在创建对象时获得一个特定的値叫这个对象被初始化
初始化不是赋值,初始化是创建变量时赋予一个初始值,而赋值是把对象当前値擦除以一个新値代替。
列表初始化
如果想定义一个名为units_sold的int类型变量并初始化为0有以下四种写法
int units_sold = 0;
int units_sold = {0};
int units_sold{ 0 }
int units_sold( 0 )
C++11新标准中可以用花括号初识化变量,这种初始化形式被称为列表初始化,无论是初始化还是赋新值,都可以使用由花括号括起来的値了
且用于内置类型变量时,如果我们用列表初始化且初始值存在丢失风险编译器将报错
long double ld =3.1415926536
int a{ ld }, b={ ld }; //错误,转换未执行,因为存在丢失信息风险
int c( ld ), d=ld; //正确但丢失部分値,因为编译器拒绝ab初始化请求且至少ld小数部分会丢失掉所以cb初始化成功但是此刻的ld是丢了小数部分的int
默认初始化
如果定义变量时没有指定初始值则变量被默认初始化,至于变量被赋予的默认值是什么由变量类型决定,同时定义变量的位置也会对此有影响。
如果是内置类型的变量未被显式初始化则値由定义的位置决定,定义于任何函数体外的变量被初始化为0。
一种例外情况是定义在函数体内部的内置类型变量将不被初始化,一个未被初始化的内置类型变量的値是未定义的,如果试图拷贝或以其他类型访问此类値将报错。
绝大多数类都支持无需显式初始化而定义对象,这样的类提供了一个合适的默认值,例如
std::string empty //empty非显式的定义了一个空串
Sales_item item //被默认初始化的Sales_item对象
注意一些类要求每个对象都显式初始化,如果没有初始化则报错
建议初始化每个内置类型的値确保程序安全
变量的声明和定义的关系
C++语言支持分离式编译机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。
为了支持分离式编译C++将声明和定义区分开来。
声明使得名字为程序所知 一个文件若想使用别处定义的名字则必须包含对那个文字的声明,而定义负责创建与名字关联的实体,定义还申请存储空间,也可能为变量赋初值。
如果想声明一个变量而非定义它,就在变量名前添加关键字extern并且不要显式地初始化变量。
extern int i ; //声明并定义i
任何包含了显式初始化的声明即成定义,如果给exterb赋初值就抵消了extern的作用变成定义
extern double pi=3.1416; //定义
在函数体内部如果试图初始化一个由extern关键字标记的变量将报错
变量只能被定义一次但是可以被多次声明
标识符
C++标识符由字母,数字,下划线组成,其中必须以字母或下划线开头,标识符没有长度限制但对大小写字母敏感
类或者说内置类型不能当标识符,用户自定义标识符不能出现两个下划线,不能下划线紧连大写字母开头,此外,定义在函数体外的标识符不能以下划线开头。
变量命名规范
- 标识符要体现实际含义
- 变量名一般用小写字母
- 用户自定义类名一般以大写字母开头
- 若标识符由多个单词组成则单词间要有明显区分
C++关键字 C++操作符代替名详情见书P43
名字的作用域
全局作用域和块作用域
嵌套的作用域
作用域能彼此包含或者说嵌套,被包含的叫内层作用域,包含别的作用域的作用域称为外层作用域,作用域中声明了某个名字被嵌套的作用域中都能访问该名字,允许在内层作用域中重新定义外层作用域已有的名字(不建议)
#include<iostream>
int reused = 42; //reused拥有全局作用域
int main()
{
int que = 0;//que具有块作用域
std::cout << reused << " " << que << std::endl;
//输出1:使用全局变量 输出42 0
int reused = 0; //新建同名局部覆盖全局
std::cout << reused << " " << que << std::endl;
//输出2:使用局部变量输出0 0
std::cout << ::reused << " " << que << std::endl;//作用域运算符::
//输出3:显式访问全局变量 输出42 0
}
复合类型
指基于其他类型定义的类型,比如引用和指针;
C++11新增一种引用为右値引用,主要用于内置类型
严格来说当我们使用术语引用指的是左値引用
引用为对象起了另外的名字,引用类型引用另外一种类型。
通过将声明符(就是标识符)写成&d形式来定义引用类型。
int ival = 1024;
int &ref =ival; //ref指向ival(是ival的另一个名字)
int &ref2; //错误 引用必须初始化
注意:一般在初始化变量时初始值被拷贝到新建的变量中,然而定义引用时程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用,且无法令初始值重新绑定到另一个对象,因此引用必须初始化。
定义了一个引用后对其进行的操作都是在与之绑定的对象上进行
ref=2; //把2赋给ref指向的对象,此处是赋给ival
int ii =ref; //相当于ii = ival;
- 为引用赋值实际是把値赋给它绑定的对象
- 获取引用的値实际上是获取与之绑定对象的値
- 以引用做初始值实际上是用与之绑定的对象的値作初始值
int &ref13 = ref; ///正确 ref13绑定到那个与ref绑定的对象ival身上
int a =ref; //利用ref绑定的对象的値初始化a,正确,i被初始化为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都是引用
除了书本第55和第534页要介绍的两种例外,,其他所有引用的类型要与之绑定的对象严格匹配,且引用只绑定在对象上而不能是字面值或者某个表达式的计算结果绑定在一起
int &ref14=10; //错误 引用类型的初始值必须是一个对象
double dval=3.1416;
int &ref15=dval; //错误,类型与与绑定类型不匹配 必须是int类型
指针
指针是指向另一种类型的复合类型,与引用类似,指针也实现了对其他对象的间接访问
- 指针本身就是一个对象,允许对指针赋值和拷贝 ,且在指针生命周期内它可以先后指向几个不同的对象。
- 指针无需在定义时赋初值,和其他内置类型一样在块作用域内定义的指针如果没有被初始化也会有一个不确定的値
定义指针方法就是将声明符写成*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的指针
注意:引用不是对象没有实际地址,所以不能定义指向引用的指针
除了P56和P534页两种例外情况,其他所有指针的类型都要和它所指向的对象严格匹配
double dval;
double *pd=&dval; //正确:初始值是double型对象的地址
double *pd2=pd; //正确, 初始值是指向double对象的指针
int *pi=pd; //错误,指针pi类型和pd类型不匹配
pi=&dval; //错误,试图把double类型对象的地址赋给int型指针
指针的値(即地址)应属于下列4种状态之一
- 指向一个对象。
- 指向紧邻对象所占空间的下一个位置。
- 空指针,意味着指针没有指向任何对象。
- 无效指针,也就是上述情况之外的値。试图拷贝或访问该値将报错,且第二三种指针同理。
利用指针访问对象
如果指针指向了一个对象则允许使用解引用符*来访问该对象。
解引用仅适用于那些确实指向了某个对象的有效指针
int ival=42;
int *p=&ival; //p存放变量ival的地址,或者说p是指向变量ival的指针
cout << *p; //由符号*得到指针p所指的对象,输出42
对指针解引用得到所指对象,如果给解引用的结果赋值就是给指针所指的对象赋值
*p=0; //由符号*得到指针p所指的对象,即可由p为变量ival赋值
cout<<*p; //输出0
像&和*这种符号可作运算符也可以作为声明的一部分出现,根据符号上下文决定其中含义
int i=42;
int &r=i; //&紧随类型名出现所以是声明的一部分,r是一个引用
int *p; //*紧随类型名出现所以是声明的一部分,p是一个指针
p=&i; //&出现在表达式中,是一个取地址符
*p=i; //*出现在表达式中,是一个解引用符
int &r2 //&是声明的一部分,*是一个解引用符
空指针
不指向任何对象,在使用指针前检查是否为空,以下列出几个生成空指针的方法
- int *p1=nullptr; //等价于int *p1=0;
- int *p2=0; //直接将p2初始化为字面值常量0;
- int *p3=NULL //等价于int *p3=0;
想用NULL这个预处理变量来给指针赋值, 需要先检查#include cstdlib, NULL在头文件cstdlib中定义,値为0。预处理变量不属于命名空间std,所以可以直接用
得到空指针最直接的方法就是用nullptr这个特殊类型的字面值来初始化指针,它可以被转化为任意其他的指针类型;另一种方法就是直接赋值字面值0来生成空指针。
用NULL和直接用0初始化指针是一样的所以最好用nullptr和尽量避免用NULL。
int zero=0;
pi=zero; ///错误,不能直接把变量赋给指针
尽量初始化所有指针
赋值和指针
指针和引用都能提供对其他对象的间接访问
int i=42;
int *pi=0; //pi被初始化但没有指向任何对象
int *pi2=&i; //pi2被初始化且存有i的地址
int *pi3; //如果pi3定义在块内则値无法确定
pi3=pi2; //都指向i
pi2=0; //现在pi2不指向任何对象了
有时候想知道赋值到底改变的是指针的値还是指针所指对象的値不太容易,记住赋值改变的永远是等号左侧的値。
pi = &ival //pi 値被改变,现在pi指向了ival
*pi =0; //解引用操作,ival値变了,但指针pi没有改变
其他指针操作
只要指针拥有了一个合法値,就能将它用在条件表达式中。
int ival=1024;
int *p=0; //合法,p是空指针
int *p2=&ival; //合法,存放ival的地址
if( p ) //p値为0所以fales
//...
if( p2 ) //p指向ival,値不是0,所以true
//...
指针如何比较大小
对于两个类型相同的指针可以用==和!=来比较它们,如果两个指针存放地址値相同则它们相等;反之它们不相等。这里地址値相等有三种可能:
- 它们都为空且都指向同一个对象。
- 或者都指向了同一个对象的下一地址
- 注意:一个指针指向了一对象,同时另一个指针指向另外对象的下一地址也可能出现指针値相等的情况即指针相等
void* 指针
是一种特殊的指针类型,可以存放任意对象的地址
double obj=3.14,*pd =&obj;
void *pv= &obj; //正确,void*能存放任意类型对象的地址,obj可以是任意类型
pv=pd //pv可以存放任意类型的指针
注意:不能直接操作void*指针所指向的对象因为我们不知道这对象具体类型,概况来说以void*的视角看内存空间仅仅是看内存空间,无法访问内存空间所存的对象。
理解复合类型的声明
变量的定义包括一个基本数据类型和一组声明符,在同一条语句中虽然基本数据类型只有一个,但声明符形式可以不同,就是说一条语句能定义出不同类型的变量。
int i=1024, *p=&i, &r=i;
// i是一个int型的整数,p是一个int型的指针,r是一个int型的引用
注意:类型修饰符&和*是声明符的一部分和数据类型无关
int * p //合法,类型说明符和声明符直接用空格隔开但是容易产生误导
基本数据类型是int不是int*,*仅仅修饰声明符p和其他没有关系
涉及指针和引用的声明一般有两种写法
- 把修饰符和变量标识符写一起,比如int *p, *p2; //p1和p2都是指向int的指针。
- 把修饰符和类型名写一起并且每条语句仅定义一个变量。
指向指针的指针
声明符中的修饰符没有个数限制,以指针为例指针是内存中的对象,有自己的地址,所以允许把指针地址再存放到另一个指针里 ,通过*个数区分指针级别,就是说,**表示指向指针的指针,***表示指向指针的指针的指针,以此类推:
int ival=1024;
int *pi=&ival; //pi是指向int型的数
int **ppi=π //ppi指向一个int型的指针
解引用int型指针得到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
const限定符
用关键字conse对变量类型加以限定使其不可更改。
const int buf=512; //输入値大小
此刻被const修饰变成常变量,任何试图为其赋值行为都将引发报错
buf=512; //错误,试图向buf写値
因为const对象一旦创建后其値不可更改,所以const对象必须初始化
const int i=get_size(); ///正确,运行时初始化
const int j=42; //正确,编译时初始化
const int k; //错误k是一个未初始化的常量
在不改变const对象的操作中还有一种初始化,就是利用对象去初始化另一个对象,则它们是不是const都无关紧要
int i=42;
const int ci=i; //正确,i値被拷贝给ci
int j=ci; //正确,ci値被拷贝给j
为了避免同一变量的重复定义,const对象被设定为仅在文件内有效,当多个文件中出现同名const变量,其实等同于在不同文件分别定义独立的变量。
如果想只在一个文件定义const, 而在其他多个文件声明且使用(共享)它,不管声明还是定义都用extern关键字
//file_1.cc
extern const int a=fcn(); //定义并初始化一个常量,该常量能被其他文件访问
//file_1.h
extern const int a; //与上一个文件里的是同一个,extern指明不是本文件独有,它的定义在别处
const的引用
把引用绑定到const对象上,称为对常量的引用,与普通引用不同的是,对常量的引用不能用作修改它所绑定的对象
const int ci=1024;
const int &ri=ci; //正确,引用的对象是常量
ri=42; //错误,ri是对常量的引用不能修改値
int &r2=ci; //错误,试图将一个非常量引用指向一个常量对象
初始化和对const的引用
引用类型必须与其所引用对象的类型一致,但有两个例外
- 在初始化常量引用时允许用任意表达式作为初始值,只要该表达式结果能正确转换成引用的类型。
- 允许为一个常量引用绑定非常量的对象,字面值,表达式
int i=42;
const int &r1=i; //正确
const int &r2=42; //正确
const int &r3=r1*2; //正确
int &r4=r1*2; //错误,r4只是普通的非常量引用
double dval =3.14;
const int &ai=dval
///类型不匹配进行如下操作类型转换
const int temp=dval; //由双精度浮点数生成一个临时的整型常量
const int &ai=temp //让ai绑定这个临时量
把引用绑定到临时量上是非法的
对const的引用可能引用一个非const的对象
int i=42;
int &r1=i; //引用ri绑定对象i
comst int &r2=i; //r2也绑定对象i,但是不允许通过r2修改i的値
r1=0; //r1并非常量,i値修改为0
r2=0; //错误,r2是一个常量引用
指针和const
指向常量的指针,不能用于修改其所指对象的値,想存放常量对象的地址,只能使用指向常量的指针。
const double pi=3.14; //pi是一个常量
double *ptr=π //错误,ptr是一个普通指针
const double *cptr =π //正确,cptr可以指向一个双精度常量
*cptr =42; //错误,不能给*cptr赋值
指针的类型必须与所指对象类型匹配,但是有两个例外:
- 允许令一个指向常量的指针指向一个非常量对象:double dval =3.14; cptr=&dval;//正确,dval値可以改变,但是不能用常量指针改变
- 常量引用和常量指针都没有规定必须指向常量还是变量,虽然不能通过常量指针修改所指向对象的値,但是没说不能用其他途径改变
const指针
常量指针必须初始化,一旦初始化完成存在指针那个对象的地址不可改变,*放const前说明指针是一个常量,不变的是指针本身的値非指向对象的値
int errNumb =0;
int *const curErr=&errNumb; //const指针指向一个变量
const double pi=3.14159;
const double *const pip=&pi //是一个指向常量对象的常量指针
从右向左阅读可以更快理解声明含义
顶层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 int &r=ci; //用于声明引用的都是底层
当执行对象的拷贝操作时顶层和底层区别明显,其中顶层不受什么影响。
i=ci; //正确,拷贝 ci的値,它是顶层,对此操作无影响
p2=p3; //正确,p2p3指向类型相同 ,p3顶层部分不影响
底层const限制不能忽视,执行拷贝操作时,拷入和拷出对象必须具有相同的底层资格,或者两个数据类型必须能够转换,一般来说非常量可以转换常量,反之不行。
int *p=p3; //错误:p3具有底层定义而p没有
p2=p3; //正确,都是底层const
p2=&i; //正确,类型发生转换,非常量可以转化成常量
int &r=ci; //错误,普通的int& 不能绑定到int ci常量上,ci不能转换成r类型
const int &r2=i; //正确,const int&可以绑定到一个普通int, i可以转换成r2类型
constexpr和常量表达式
常量表达式指値不会改变且在编译过程就能得到计算结果的表达式,比如字面值表达式,用常量表达式初始化的const对象也是常量表达式
- 一个对象或表达式是不是常量表达式根据它的数据类型和初始値共同决定
const int max_files=20; //max_files是常量表达式
const int limit=max_files+1; //limit是常量表达式
int staff_size=27; //不是常量表达式
const int sz=get_size(); //没运行不知道返回结果所以不是
constexpr变量
C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的値是否是一个常量表达式,声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。
constexpr int mf=20; //20是一个常量表达式
constexpr int limit=mf+1; //mf+1是一个常量表达式
constexpr int sz=size(); //z=只有当size是一个constexpr函数时才是
一般来说如果你认定变量是一个常量表达式就把它声明成constexpr类型
- 字面值类型(显而易见,容易得到),比如算术类型,引用,指针都属于字面值类型,自定义类Sales_item,IO库,string类型不属于字面值类型也不能定义成constexpr。
- 一个constexpr指针初始值必须是nullptr或0,或者是存储于某个固定地址中的对象。
- 函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr不能指向这样的变量,相反函数体外的对象其地址固定不变,能用来初始化constexpr指针。
- 且允许函数定义一类有效范围超出函数本身的变量,这类变量和函数体之外的变量一样具有固定地址,因此constexpr引用或指针能绑能指向这样的变量。
指针和constexpr
在constexpr声明中定义了一个指针,限定符constexpr仅对指针有效,与指针所指对象无关
const int *p=nullptr; //p是一个指向整型常量的指针
constexpr int *q=nullptr; //q是一个指向整数的常量指针,constexpr把它定义的对象置为顶层const
constexpr指针既可以指向常量也可以指向非常量
constexpr int *np=nullptr; //np是一个指向整数的常量指针,其値为空
int j=0;
constexpr int i=42; //i是整型常量
//i和j都必须定义在函数体外
constexpr const int *p=&i; //p是一个常量指针,指向整型常量i
constexpr int *p1=&j; //p1是一个常量指针,指向整数j
类型别名
是一个名字,是某种类型的同义词,简化复杂类型名,类型别名和类型名等价
- 有两种方法可用于定义类型别名,传统方法是用typedef
typedef double wages; //wages是double同义词
typedef wages base, *p; //base是wages也就是double同义词,p是double*同义词
- 另一种方法是新标准里的,使用别名声明来定义类型别名。
using SI=Sales_item; //SI是Sales_item的同义词
把左侧等号名字规定成等号右侧类型的别名
如果某个类型别名指代的是复合类型或者常量,例如下面的声明语句用到类型pstring,实际上是char*的别名
typedef char *pstring;
const pstring cstr=0; //cstr是指向char的常量指针
const pstring *ps //ps是一个指针,它的对象是指向char的常量指针
注意 const char*基本数据类型是指针,是指向char的常量指针,而const char是基本数据类型,声明了一个指向const char的指针
auto类型说明符
auto能让编译器通过初始值推算变量的类型,所以auto定义的变量必须有初始值。
- 使用auto也能在一条语句中声明多个变量,一条语句只能有一种数据类型所以语句中所有变量类型必须一致。
auto i=0, *p=&i; //正确,i是整型,p指向i是整型指针
auto sz =0, pi=3.14; //错误类型不一致
- 初始值和auto类型不一样时编译器会作出转换比如引用
int i=0, &r=i;
auto a=r; //a是一个整数(r是i别名且i是整数,a->r->i)
- auto一般会忽略掉顶层const,保留底层const。
const int ci=i, &cr=ci;
auto b =ci; //b是一个整数(ci的顶层特性被忽略掉了)
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,初始值中的顶层const仍然保留,如果给初始值绑定一个引用则此时的常量不再是顶层常量,同一语句初始值注意是同一种类型
auto &g=ci; //g是一个整型常量的引用,绑定到ci
auto &h=42; //错误,不能给非常量引用绑定字面值
const auto &j;=42; //正确,可以给常量引用绑定字面值
auto k=ci, &l=i; //k是整数,l是整型引用
auto &m=ci, *p=&ci; //是一个整型常量引用,p是指向整型常量的指针
auto &n=i; *p2=&ci; //错误,i类型是int,p2类型是const int
decltype类型指示符
有时候遇到这种情况,希望从表达式推断出要定义的变量的値但不希望通过表达式的値初始化变量,那么C++11引入了decltype,其作用是选择并返回操作数的数据类型,却不实际计算表达式的値
decltype (f()) sum =x; //sum类型就是函数f的返回类型
decltype处理顶层和引用方式与auto有些许不同,如果使用的表达式是变量返回的类型包括顶层和引用在内
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是一个引用必须初始化
引用从来都是作为对象的同义词出现,只有用在decltype是例外
decltype和引用
如果decltype使用的表达式不是一个变量,则返回表达式对应结果的类型。
有些表达式向decltype返回一个引用类型,意味着该表达式的结果对象能作为一条赋值语句的左値
//decltype的结果可以是引用类型
int i=42, *p=&i, &r=i;
decltype(r+0) b; //加法结果是int ,因此b是一个未初始化的int
因为r是一个引用,所以decltype( r )j结果是引用类型,如果想让结果类型是r所指向的类型,就把r当做表达式一部分,如r+0,此刻表达式结果是一个具体値而非引用
decltype (*p) c; //错误 ,c是int&,必须初始化
如果表达式是解引用操作,则decltype将得到引用类型,因为解引用指针得到指针所指对象,还能给它赋值,所以结果类型是int&,不是int
//表达式如果加上括号结果是引用
decltype (( i )) d; //错误,d是int&,必须初始化
decltype ( i ) e; //正确,e是一个未初始化int
注意双层括号永远是引用,单层括号只有本身是引用才是引用,比如引用和指针
对于decltype所用的表达式来说注意有无括号的区别
- 如果变量名加上一对括号,则得到类型和不加括号时不同。
- 如果用不加括号的变量,则得到的结果是该变量的类型。
- 如果给变量加上一层或多层括号,编译器将其看成表达式所以就会得到引用类型
自定义数据结构
从基本层面理解,数据结构就是把一组相关的数据元素组织起来然后使用它们的策略和方法。比如Sales_item类就是一个数据结构 。C++允许用户以类形式自定义数据结构。
struct Sales_data //以关键字struct开始后面接类名和类体(类体部分可为空)
{
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0;
//类内部名字可和外部定义的名字重复,但内部定义的名字必须唯一
};//类体结束必须以分号结尾,因为类体后面可以紧跟变量名以示对该类型对象的定义
struct Sales_data{/*...*/} a,b,*p;
//上下两条互相等价 但下面那种写法可能更好
Sales_data {/*...*/};
Sales_data a,b,*p;
类数据成员
类体定义类的数据成员,我们的类只有数据成员。类的数据成员定义了类的对象的具体内容。每个对象都有自己的一份数据成员拷贝。修改一个对象的数据成员不会影响其他Sales_data对象。
定义数据成员和定义普通变量一样基本类型后紧跟一个或多个声明符。比如Sales_data
- 一个名为bookNo的string成员 /初始化为空字符串
- 一个名为units_sold的unsigned成员 /0
- 一个名为revenue的double成员 /0
新标准规定可以为数据成员提供类内初始值用于初始化,没有初始值的数据成员被默认初始化,限制就是不能用圆括号初始化,放花括号里或者等号右侧都行.
在没学class之前都用struct定义自己的数据结构。
自定义Sales_data类型
-
添加两个Sales_data对象
-
自定义头文件
为了确保各个文件中类的定义一致,类通常被定义在头文件中,且类所在的头文件名字应该与类名字一致
-
预处理器概述
确保文件多次包含仍能安全工作的常用技术是预处理器,之前已经用到#include预处理功能,当预处理器看到#include标记时会用指定的头文件内容代替#include
还有一种是头文件保护符(依赖于预处理变量),预处理变量有两种状态:已定义和未定义。
#define指令把一个名字设定为预处理变量,另外两个分别检查某个指定的预处理变量是否定义。
- #ifdef当且仅当变量已定义时为真
- #ifndef当且仅当变量未定义为真
一旦检查结果为真则执行后续操作直至遇到#endif为止。
#pragma once
#include <iostream>
#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
//第一层包含Sales_data.h文件#ifndef检查结果为真预处理将顺序执行后面的直到遇到#endif为止
//此刻Sales_data.h値变为已定义且値拷贝到我们程序中
//后面再包含Sales_data.h则#ifndef检查为假忽略后面的部分直到#endif
//预处理变量无视C++语言中关于作用域的规则
//头文件保护符和预处理变量必须唯一,通常大写
//保护符很重要不管程序重不重要都最好写
#include<iostream>
#include<string>
#include"Sales_data.h"
int main()
{
Sales_data data1, data2;
//第一步读入data1和data2的代码
//第二步检查data1和data2是否相同的代码
//第三步如果相同求data1和data2的总和
double price = 0; //书的单价 用于计算销售收入
//输入第一笔交易:ISBN,销售数量,单价
std::cin >> data1.bookNo >> data1.units_sold >> price;
//计算销售收入
data1.revenue = data1.units_sold * price;
//输入第二笔交易:ISBN,销售数量,单价
std::cin >> data2.bookNo >> data2.units_sold >> price;
//计算销售收入
data2.revenue = data2.units_sold * price;
//接下来输出两个对象的和
if (data1.bookNo == data2.bookNo)
{ //如果书单号相等把销售量相加
unsigned totalCnt = data1.units_sold + data2.units_sold;
//计算总收入
double totalRevenue= data1.revenue + data2.revenue;
//输出书单 总销售量 总销售额
std::cout << data1.bookNo << " " << totalCnt << " " << totalRevenue << " ";
if (totalCnt != 0) //如果总销售量不为0
{
std::cout << totalRevenue / totalCnt << std::endl;
//输出平均价格=(总销售额/总销售量)
}
else
{
std::cout << "(no sales)" << std::endl;//销售量为0打印无销售
}
return 0;//标示成功
}
else //如果书单号不相等
{
std::cerr << "Data must refer to the same ISBN"
<< std::endl;
return -1; //标示失败
}
}