C++系统学习一:基本数据类型和变量
程序语言
程序语言最基本的特征
- 整型、字符型等内置类型
- 变量,用来为对象命名
- 表达式和语句,操纵上述数据类型的具体值
- if等控制结构
- 函数,定义可供随时调用的计算单元
程序语言的扩展
- 自定义数据类型
- 封装的库函数
NOTE:
C++的对象类型决定了能对该对象进行的操作,一条表达式是否合法依赖于其中参与运算的对象的类型。
C++是一种静态数据类型语言,它的类型检查发生在编译时,编译器必须知道每个变量的数据类型。而Python等是在程序运行时检查数据类型。
2.1 变量和基本类型
数据类型是程序的基础:决定数据的意义以及可以在数据上执行的操作。
2.1.1 算术类型
NOTE:
除了布尔和扩展字符类型,其他整型都分为带符号和无符号两种
如何选择类型
- 明确知道不可能为负时,选用无符号类型
- 使用int执行整数运算
- 算术表达式中不要使用char或bool,只有在存放字符或布尔值时才使用它们,如果需要使用一个不大的整数时要使用char时,要明确指定它的类型是signed char或者unsigned char
- 执行浮点运算选用double
2.1.2 类型转换
- 非布尔类型与布尔类型之间相互转换,0为false,非0为true
- 浮点数赋给整数,近视处理
- 整数赋给浮点数,小数部分记为0
- 给无符号类型一个超出其表示范围的值时,结果是初始值对无符号类型表示树枝总数取模后的余数
- 赋给带符号类型一个超出它的表示范围的值时,结果是未定义的。
负数取模
负数要加上被除数的整数倍,将其变为大于0之后,再进行取模运算。如-1%256=(-1+256)%256=255.
1 unsigned char c=-1; 2 int k=c; 3 cout<<k<<endl; 4 5 输出结果是: 6 255
含有无符号类型的表达式
当一个段数表达式中既有无符号数又有int时,int会转换为无符号数,结果是对其进行取模运算。尤其在循环中要注意无符号类型最好不要作为循环控制变量
for(unsigned i=10;i>0;i++) { ; } 这是个死循环,因为 i 永远不可能为负
NOTE:切勿混用带符号和无符号类型
2.1.3 字面值常量
每个字面值常量都对应一种数据类型,字面值常量的形式和值决定了它的数据类型。
整型和浮点型字面值
- 整型字面值
有十进制、八进制、十六进制三种形式,其中八进制以0开头,十六进制以0x开头。
整型字面值具体数据类型由它的值和符号决定。默认情况下,十进制是带符号数,八进制和十六进制可能是带符号,也可能是无符号数。
十进制字面值的类型是int,long,long long中能容纳下当前值的尺寸最小的那种类型。
八进制和十六进制是int,unsigned,long,unsigned long,long long,unsigned long long中能容纳当前值的尺寸最小的那个。
当一个字面值连与之相关联最大的数据类型都放不下时,将产生错误。
short 没有对应的字面值。
- 浮点型字面值
表现为一个小数或以科学计数法表示的指数,E或e标识:3.14E0
默认浮点型字面值是一个double
字符和字符串字面值
- 字符字面值:单引号括起来的一个字符
- 字符串字面值:双引号括起来的零个或多个字符。
字符串字面值实际上是由常量字符构成的数组,编译器在每个字符串后面会添加一个空字符'\0'
转义序列
两类程序员不能直接使用的,就需要使用转义序列来使用。\
指定字面值的类型
布尔字面值和指针字面值
- 布尔字面值:true和false
- 指针字面值:nullptr
2.2 变量
变量提供一个具名的、可供程序操作的存储空间。变量的数据类型决定着变量所占空间的大小和布局方式、该空间能存储的值的范围,以及变量能参与的运算。
2.2.1 变量定义
- 初始值
初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。
- 列表初始化
用花括号{ }来初始化变量称为列表初始化。如果对内置类型变量采用列表初始化时要注意,如果初始值存在丢失信息的风险,编译器将报错。但用圆括号()的方式则不会。
double a=3.1415926; int b{a}; //将会报错 int c(a); //不会报错,c=3
- 默认初始化
没指定初始值的变量会被默认初始化,默认值由变量类型决定,定义的位置也会对此有影响。内置类型的默认初始化,如果定义在任意函数之外则为0,在函数体内定义,则不会被初始化,是未定义的。
2.2.2 变量声明和定义的关系
- 变量声明存在的原因
为了支持分离式编译,使得名字被程序所知,在文件间共享代码。
- 怎么进行变量声明:只声明不定义在变量名前添加externa,不要显式地初始化变量,否则extern会失效。
extern int i;
NOYE:变量可以多次声明,但只能被定义一次。
2.2.3 标识符
由字母、数字和下划线组成,且开头必须是字母或下划线。
2.3 复合类型
基于其他类型定义的类型
2.3.1 引用
- 定义:变量的别名
int a=0; int &d=a; //定义a 的引用d
引用必须初始化
- 引用不是对象,只是已经存在的对象的别名而已
- 只能定义对象的引用,而不能定义字面值或表达式的引用(引用实质上是对内存某块特定空间取别名,只有对象才在内存中有分配特定可寻的空间,像字面值和表达式都是没有实际的内存空间的,因此不能定义它们的引用)
- 除两种特殊情况外,其他引用的类型都要和对象完全匹配
2.3.2 指针
指针和引用的区别
- 指针本身是一个对象,在内存中有存在,而引用不是一个对象
- 指针无需在定义时赋初值,而引用必须赋初值
获取对象的地址
int a=10; int *p=&a;
指针类型除两类特殊情况外要和它指向的对象类型严格匹配。
指针值:只有四种情况
- 指向一个对象
- 指向紧邻对象所占空间的下一个位置
- 空指针,不指向任何对象
- 无效指针
利用指针访问对象:解引用符*
空指针
生成空指针的方法
int *p1=nullptr; int *p2=0; int *p3=NULL;
尽量使用前两种方式生成空指针,第三种需要包含第三方库,NULL实质也是0,所以一般不使用第三种方式。
NOTE:不要把int变量直接赋值给指针,即使变量等于0也不行。
int a=0; int *p=a; //错误,虽然a=0,也不行
其他指针操作
可以用相等操作符(==)或不等(!=)来比较指针,指向同一个对象则相等。
void*指针
void*指针可用于存放任意对象的地址,因此,不能直接操作void*指针所指的对象。
2.3.3 理解复合类型的声明
定义多个变量
指向指针的指针
指向指针的引用
int i=42; int *p=&i; int *&r=p;
2.4 const限定符
const对象一旦创建后,其值就不能再改线,因此,必须初始化。初始值可以是任意复杂的表达式。
NOTE:默认情况下,const对象仅在文件内有效。
解决办法:对于const变量不管是声明还是定义都添加extern关键字,这样只需定义一次就可以了。
//file1.cpp定义并初始化了一个常量,该常量能被其他文件访问 extern const int a=0; //file1.h头文件 extern const int a;
2.4.1 const的引用
把引用绑定到const对象上,称为对常量的引用,与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象。
const int a=10; const int &r=a;
int &r2=a; //错误,r2是非常量引用,其可能会改变所绑定的对象
初始化和对const的引用
引用类型必须与其所引用的对象类型一致,有两个例外,第一个例外是:在初始化常量引用时允许用任意表达式作为初始值,尤其允许为一个常量引用绑定非常量的对象、字面值,甚至是一个表达式,只要该表达式的结果能转换成引用的类型即可。如果不能转换则会存在使引用绑定到一个临时变量的问题,如下:
double dval=3.14; const int &r=dval; =================== 在绑定之前dval会作一个转换 cosnt int temp=dval; 然后将这个temp和r绑定 const int &r=temp; 但是temp是一个临时量,在内存中并不存在,所以不行
对const的引用可能引用一个并非const的对象
常用引用仅仅限定了引用不能有修改对象的操作,但对引用的对象本身是否是常量并未限定,所以可以通过其他方式修改所绑定的对象,只要不通过常用引用即可。
int a=32; int &b=a; const int &c=a; b=0; //正确 c=0; //错误
2.4.2 指针和const
指向常量的指针不能用于改变其所指对象的值,要想存放常量对象的地址,只能使用指向常量的指针。
指针的类型必须与其所指对象的类型一致的第一种例外情况是:允许令一个指向常量的指针指向一个非常量对象。
const double pi=3.14; double *ptr=π //错误,ptr是一个普通指针 const doulbe *cptr=π //正确 *cptr=42; //错误 double dval=3.14; cptr=&dval; //正确,但是不能通过cptr改变dval的值
小结
判断引用和指针与const有关的赋值是否可行的一个依据:看赋值之后的引用和指针是否具有修改底层对象的能力,如果底层对象本身不是const允许修改则没关系,例如上述赋值之后的cptr本身不具有修改底层对象的能力,且dval本身不是const,可以被修改,所以将其赋值给const指针没关系,但是如果赋值之后的引用和指针有修改底层对象的能力,并且底层对象是const,不能被修改,此时则不行,例如上述中将ptr指向pi,此时ptr具有修改底层对象的能力,但底层对象是const,不能被修改,因此错误。
2.4.3 顶层const
顶层const表示指针本身是个常量,底层const表示指针所指对象是一个常量。
如何判断顶层const和底层const
看对象本身是否是const即可,本身是const则是顶层const,本身不是const,但所指对象是const则是底层const。
顶层const和底层const对拷贝的影响
顶层const对拷贝影响不大。但底层const对拷贝的影响不可忽略。
记住一点:常量可以赋值给非常量,但非常量不能赋值给常量。看拷贝之后,新的变量会不会有可能造成底层权限的更改。
2.4.4 constexpr和常量表达式
常量表达式是指不会改变并且在编译过程就能得到计算结果的表达式。
一个对象(或表达式)是不是常量表达式由它的数据类型嗯哼初始值共同决定。
int a=24; //不是常量表达式,不是const int const int s=get_size(); //不是常量表达式,初始值不是常量
constexpr变量
允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。
字面值类型
在编译时就得到计算,值显而易见,容易得到,称为字面值类型。
算术类型、引用和指针都属于字面值类型。
一个constexpr指针的初始值必须是nullptr或0,或者是存储于某个固定地址中的对象。
函数体内的变量不是存放在固定地址中,constexpr指针不能指向这样的变量,而函数体外的变量存放在固定地址中则可以。
指针和constexpr
在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指对象无关。
const int *p=nullptr; //p是一个指向常量整数的指针 constexpr int *p=nullptr; //p是一个指向整数的常量指针
2.5 处理类型
2.5.1 类型别名
- typedef
typedef double wages; typedef wages base, *p; //p是double*的同义词
- using
using SI=double;
2.5.2 auto类型说明符
auto让编译器通过初始值来推算变量的类型,因此,auto定义的变量必须初始化。
复合类型、常量和auto
编译器以引用对象的类型作为auto的类型
auto一般会忽略掉顶层const,同时底层const会保留下来。
int i=0; const int ci=i,&cr=ci; auto b=ci; //b是一个整数,ci的顶层const去掉了
如果希望推断出的auto类型是一个顶层const,需要明确指出
const auto f=ci; //f推断类型是cosnt int
2.5.3 decltype类型指示符
从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。
因此引入decltype说明符,其作用是选择并返回操作数的数据类型,编译器只分析它的类型,但不实际计算表达式的值。
decltype(f()) sum=x; //sum的类型就是函数f的返回类型
decltype处理顶层const和引用的方式与auto有些与不同。它会保留顶层const和引用。
decltype和引用
decltype((variable))(注意是双层括号)的结果永远是引用,而decltype(variable)的结果只有当variable本身是引用时才会是引用。
int i=42; decltype((i)) d; //错误,d是int&引用类型,必须初始化 decltype(i) e; //正确,e是int类型
2.6 自定义数据结构
预处理器
确保头文件多次包含仍能安全工作的常用技术是预处理器
头文件保护符:依赖于预处理变量,预处理变量由两种状态:已定义和未定义
#define指令把一个名字设定为预处理变量,#ifdef和#ifndef用来判断预处理变量是否已定义,判断后用#endif结束
#ifdef SALES_H_ #define SALSE_H_ ...... .... #endif