🍪第2章 变量和基本类型
概述 c ++定义了几种基本的内置类型(如字符、整型、浮点数等),同时提供自动以数据类型的机制。
🍮2.1 基本内置类型
基本内置类型包括算数类型和空类型。算数类型包括字符、整型数、浮点数和布尔值。
🍭2.1.1 算术类型
类型 | 含义 | 最小尺寸 |
---|---|---|
bool |
布尔类型 | 8bits |
char |
字符 | 8bits |
wchar_t |
宽字符 | 16bits |
char16_t |
Unicode字符 | 16bits |
char32_t |
Unicode字符 | 32bits |
short |
短整型 | 16bits |
int |
整型 | 16bits (在32位机器中是32bits) |
long |
长整型 | 32bits |
long long |
长整型 | 64bits (是在C++11中新定义的) |
float |
单精度浮点数 | 6位有效数字 |
double |
双精度浮点数 | 10位有效数字 |
long double |
扩展精度浮点数 | 10位有效数字 |
类型选择
- 当明确知道数值不为负时,选用无符号类型
- 在实际应用中,一般选用
int 类型
,如果大的话选用long long
类型,一般short与 long不用 - 算术运算符,不要使用
char
和bool
.因为char类型在一些机器上有符号,而到了另外机械就无符号。如果需要使用char,就指明是signed char
或者unsigned char
- 浮点数用
double
,因为float精度可能不够。long double没必要,消耗内存。
🍭2.1.2 类型转换
概述几种类型转换:
- 非布尔值赋给布尔型,初始值为0则结果为false,否则为true。
- 浮点数转换为整数,结果
仅保留小数点前面的部分。
- 给无符号类型赋值时,如果超出它的表示范围,则
结果是初始值对无符号类型表示数值总数取模后的余数
,例如-1 赋给 8 位 unsigned char 的结果是 255。 - 给带符号类型赋值超出它的表达式,结果是
未定义
的,程序可能会崩掉
建议:尽量避免无法预知和依赖实际环境的行为
取模与取余区别:取余运算在取c值时,向0方向舍入,而取模运算在取c时,向负无穷方向舍入
- 求整数商:c = [a / b]
- 计算模: r = a - c * b;
// 举例 那题目当中的-1 举例
-1 MOD 256
第一步求商:
c = [a / b] = -1 / 256 = -1 //结果为-0.0039,向负无穷取整为1
第二步求模:
r = a - c * b = (-1) - (-1) * 256 = 255
//或者这样理解
计算机在存储-1时是存储的-1的补码,对于8位unsigned char来说-1的原码为1000 0001,它的反码为(符号位不变,其他位置取反)1111 1110,则补码为(补码= 反码+1),1111 1111,换成十进制整数为255
含有无符号的整数
- 一个表达式中既有无符号数又有int值时,int会被转换成无符号数。
- 无符号减无符号数,结果还是无符号数,如果是负值就等于取模后的值
总结:无符号参与运算时,结果肯定不是负数,如果是负数则需要对结果取模
🍭2.1.3 字面值常量
整型和浮点型字面值整型字面值中 0 开头的整数是 8 进制,0x 开头的整数是十六进制。
浮点型字面值可以用小数或科学计数法表示,科学计数法中的指数部分用 E 或 e 标识。
3.1415926 0. 0e0 .001 3.14159E2
字符和字符串字面值
'c' 字符字面值
"Hell world" 字符串字面值比实际值多1,因为它的最后多一个'\0'(空字符)
hello world被看做一个整体,只要字符串字面值位置仅有空格、缩进、换行符分隔,就看做一个整体
转义序列
换行符:\n | 横向制表符:\t | 报警符:\a |
---|---|---|
纵向制表符:\v | 退格符:\b | 双引号:\" |
反斜线:\\ | 单引号:\' | 问号:? |
回车符:\r | 进纸符:\f |
布尔字面值和指针字面值
true false 是布尔类型的字面值
nullptr 是指针字面值
🍮2.2 变量
变量 :提供一个具名的、可供程序操作的存储空间
对象 :一块能存储数据并具有某种类型的内存对于c++,而言变量和对象可以互换使用
🍭2.2.1 变量的定义
初始化
对象在创建的过程中就获得了一个值,此时这个对象被初始化。
初始化和赋值的区别:
- 初始化不是赋值,初始化是创建一个变量时赋予其一个初始值
- 赋值是把对象的当前值擦除,而以一个新值来代替。
列表初始化
用一对{ }来表示列表初始化
int i = 0;
int i = {0};
int i {0};
int i (0);
特点
:如果使用列表初始化,且初始值存在,丢失情况,则编译器会报错
long double ld = 3.141592;
int a{ld}; //错误,信息丢失,精度问题,转换失败
int c(ld), d=ld; //正确
默认初始化
- 定义在函数体内的内置类型的对象没有初始化,则其值为未定义。
- 定义在函数体外的内置类型的对象没有初始化,则其值为0(string为空,int型为0)
全局变量没有初始化,系统会自动初始化为0,局部变量没有初始化,未定义,会报错。建议初始化每一个内置类型的变量。
🍭2.2.2 变量声明和定义
声明:使得名字让程序知道 (不申请存储空间)
定义:创建与名字关联的实体(也就是初始化,需要申请存储空间)
要声明一个变量加 extern,声明变量不能赋值。
extern int i; // 声明 i
int j; // 声明并定义j;
extern int i = 1; // 定义 i,初始化抵消了 extern 的作用。
变量能且只能被定义一次,但是可以被多次声明c++是静态类型语言,其含义是在编译阶段检查类型。
🍭2.2.3 标识符
标识符组成:字母、数字、下划线。不能以数字开头,对大小写敏感。标识符的长度没有限制。
变量命名规范:
- 标识符要体现其实际含义。
- 变量名一般用小写字母,index,不要使用Index或INDEX
- 用户自定义的类型一般以大写字母开头。Sales_item
- 包含多个单词的标识符,使用驼峰命名法或使用下划线连接不同单词。
- 对于嵌套作用域,可以在内层作用域中重新定义外层作用域已有的名字,但是最好不要这样做。
🍭2.2.4 名字作用域
- 第一次使用变量时再定义它。
- 嵌套作用域,作用域一旦声明了,那么它所嵌套的所有作用域中都能访问该名字,允许在内层作用域中重新定义外层已有的作用域
实际就是:局部变量覆盖全局变量
🍮2.3 复合类型
复合类型是基于其他类型定义的类型,例如指针和引用
🍭2.3.1 引用
引用就是给对象起别名
引用的本质就是指针常量,指针常量就是指针所指对象的值可以变,但是指针所指的对象不可以变,因此他和一个对象绑定好后,通过引用来改变值,但是不可以在重新绑定到另一个对象上
引用总结
引用必须初始化
引用的初始值必须是一个对象,不能是字面值,如果想要是字面值需要加const
引用不是对象,所以不能定义引用的引用,也不能定义引用的指针
- 对引用的所有操作都对与之绑定的对象的操作
引用必须绑定一个对象上,且不能重新绑定到另一个对象上
- 引用的类型与绑定的类型要匹配
int i = 0;
int &r = i;
🍭2.3.2 指针
在块作用域内,指针如果没有被初始化,值将不确定。
指针类型被用于指定它所指向的对象的类型,二者要匹配
int i = 0;
double *dp = &i; // 错误
int *ip = i; // 错误,但 int *ip = 0; 是正确的
int *p = &i; //正确
指针与引用区别
- 指针是一个对象而引用不是
- 指针可以重定向,引用不可以;
- 有指向指针的指针,没有指向引用的引用;
- 指针不需要在定义时赋初值而引用需要。
- 不能定义指向引用的指针。可以定义指向指针的引用。
int *p;
int* &r = p; // r是对指针p的引用(指针的引用)
int i = 1;
int &r = i;
int& *p = &r //指向引用的指针,错误的,因为r不是对象,没有地址。
利用解引用符(*)可以访问指针指向的对象。
int i = 42;
int *p = &i;
cout << *p <<endl; 结果为42
空指针
int *p = nullptr; // 推荐这种
int *p = 0;
int *p = NULL; // NULL 是在头文件 cstdlib 中定义的预处理变量,值为 0。
建议初始化所有指针
赋值和指针
int i = 42;
int *p = &i;
*p = 30;
说明:
- 指针所指的对象变了(i 的值变了)
- 指针本身并没有变化 (i 的地址没变),
指针指的是地址,而不是值
void*指针
void* 指针是特殊的指针类型,可以存放任意对象的地址。它的用处比较有限。(不使用)
🍭2.3.3 理解复合类型的声明
一条声明语句是由一个基本数据类型和后面的声明符列表组成的。
引用符 & 和指针符 * 都是类型说明符,类型说明符是声明符的一部分。
int i = 1024, *p = &i, &r = i;
int *p, p2; //p是指向int的指针,p2是int
指向指针的指针
int i = 1024;
int *p = &i; //p指向一个int 型数
int **p1 = &p; //p1指向一个int 型指针
指向指针的引用
引用本身不是一个对象,所以没有指向引用的指针,但是指针是对象,存在对指针的引用
int i = 42;
int *p ; //p是int型指针
int *&r = p; //指向指针的引用
-------------------------------------------------------------------------------
int i = 1;
int &r = i;
int& *p = &r //指向引用的指针,错误的,因为r不是对象,没有地址。
想要理解r的类型到底是什么,可以从右往左读
,离变量最近的符号(本例中是&r符号),因此r是一个
引用,声明符以外的部分可以确定
r引用的类型是什么。本例中
&左边是*,所以r引用的是一个
指针`
🍮2.4 const限定符
const对象必须初始化
,因为const对象一旦创建,其值不能改变- const指向的对象,不能改变,但是仍然可以和int一样,进行
运算
- 默认情况下,const对象
只在文件内有效
- 如果想要在不同文件中
共享const
,必须在定义变量之前添加exten关键字
🍭2.4.1 const的引用
引用必须初始化
,因此常量引用也必须初始化。- 常量引用是对 const 的引用,对象可以是
常量
也可以是非常量
- 不能用非常量引用指向一个常量对象。可以用常量引用指向一个非常量对象。
- 引用的类型必须与其所引用对象的类型一致,
有俩种情况特殊,下面详细说
int i = 42; //非常量
const int j = 42; //常量
const int &p = i; //正确
const int &p = j; //正确,常量引用的对象既可以是常量也可以是非常量
const int &p = 10; //正确
int &p = 10; //错误
//非常量引用不能指向常量对象
const int p = 66;
int &r = p; //错误
引用的类型必须与其所引用对象的类型一致,有俩种情况特殊,下面详细说
- 初始化常量引用时允许用任意表达式作为初始值(包括常量表达式),只要该表达式结果可以转换为引用的类型。
- 当用常量引用绑定一个非常量对象时,不能通过引用改变引用对象的值,但是可以通过其他方式改变值。常量指针也一样。
举例
double val = 3.1415;
const int &p = val; //正确
int i =42;
int &r1 = i; //引用r1绑定对象i
const int &r2 = i; //常量引用r2 绑定i,此时不允许通过r2来修改i的值,但是可以用r1来修改i 的值
r1 = 0; //正确,i 的值修改为0
r2 = 0; //错误
🍭2.4.2 指针和const
- 指向常量的指针和指向常量的引用用法相似,既可以指向常量,也可以指向非常量,如果指向非常量,那莫这个非常量对象的值不可以不可以通过指针,来改变,可以通过其他途径改变。
指针常量与常量指针区别
- 指针常量:指针所指的对象值
可以变
,但是指针所指的地址不可以变
int * const p = &r - 常量指针:指针所指的对象的值
不可以变
,但是指针所指的地址可以变
const int * p = &r
🍭2.4.3 顶层const
顶层const表示指针常量
,指针本身是一个常量,地址不能变,但是值可以修改。底层const表示常量指针
, 指针所指的值是一个常量,不可以修改,但是地址可以变。- 顶层const的指针,表示该指针是const对象,因此必须初始化,而底层const不需要初始化。
顶层const对任意数据类型都适用,但是底层const只用于引用和指针
指针类型既可以是顶层 const 也可以是底层 const,因为引用只能是底层 const,常量引用为底层 const,不存在顶层 const 的引用。
<br/>
怎么区分是顶层const还是底层const,最简单的方法
- 对于指针和引用来说,顶层const在右边,底层const在左边。对于其他类型来说,全部都是顶层cosnt.(
记住就能理解了
)
const int* const p1 = p2; // 从右向左读,右侧const是顶层const,表明p3是一个常量,左侧const是底层const,表明指针所指的对象是一个常量
const int* p3 = &c; // 这是一个底层const,允许改变 p2 的值(常量指针)----const在左边为底层
int* const p4 = &i; // 这是一个顶层const,不能改变 p1 的值(指针常量)----cosnt在右边为顶层
执行对象的拷贝操作时,不能将底层 const 拷贝给非常量,反之可以,非常量将会转化为常量。
🍭2.4.4 constexpr和常量表达式
常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。
字面值属于常量表达式,由常量表达式初始化的 const 对象也是常量表达式。
const int i = 32 //常量表达式
const int j = i + 1 // 常量表达式
const int sum = get_size() //不是常量表达式,尽管 sum 是常量,但它的具体值等到运行时才知道。
constexpr变量
- 在一个复杂的系统中,很难分辨一个初始值到底是不是常量表达式,此时通过把变量声明为
constexpr
类型,使得编译器自动帮我们验证。 由 constexpr 声明的变量一定是常量不能是普通函数,必须用常量表达式初始化。
constexpr int sz = size(); //只有当 size() 是一个 constexpr 函数时这才是一条正确的声明语句。
constexpr int mf = 20; //常量表达式
constexpr int limit = mf + 1; // mf + 1是常量表达式
字面值类型
- 算术类型、引用、指针都属于字面值类型,自定义类则不属于。
- cosntexpr 指针的初始值必须是
nullptr 或 0 或存储于固定地址的对象
。 - 函数体之外的对象和静态变量的地址都是固定不变的.,可以用constexpr初始化,而函数体内的变量不能用contexpr.
指针和constexpr
contexpr所定义的对象都是顶层const
,仅对指针有效
const int *p = nullptr; // p 是一个指向整型常量的指针(指针常量)底层const
constexpr int *q = nullptr; // q 是一个指向整数的常量指针(常量指针)定义成了顶层const
const与constexpr区别
- constexpr限定了变量是编译器常量,即变量在编译过程中就可以得到结果。
- const 未区分是编译器常量还是运行期常量。即 const 变量可以在运行期间初始化,只不过是初始化后的值就不能再改变了。
- constexpr 变量是真正的“常量”,而 const 现在一般只用来表示 “只读”。
🍮2.5 处理类型
🍭2.5.1 类型别名
目前有俩种方法来定义类型别名
- typedef
- using
typedef int wages; // 使用 typedef 关键字
using wages = int; // 使用 using 关键字进行别名声明
typedef wages base, *p; // base 是 double 的别名,p 是 double* 的别名。
指针、常量和类型别名
typedef char* pstring;
const pstring cstr = 0; // 注意:const 是一个指向 char 的常量指针。不能采用直接替换的方式将其理解为 const char* cstr = 0,这是错误的。
🍭2.5.2 auto类型说明符
- auto让编译器自动帮你识别,表达式所属的类型
- 但是与int 不同的是,auto让编译器通过初始值来推算变量的类型,所以
auto 必须要有初始值
- auto 可以在一条语句中声明多个变量,但是多个变量必须是同一个基本数据类型(整型与整型指针和整型引用算一个类型)。
int vall, sum;
auto item = vall + sum //自动推断为int类型
复合类型、常量和auto
编译器推断出的 auto 类型有时和初始值并不一样,编译器会进行适当的调整:
- auto 根据引用来推断类型时会以引用对象的类型作为 auto 的类型。
- auto 一般会忽略掉顶层 const,因此对于非指针类型的常量对象,auto 推断出的结果是不含 const 的。如果希望 auto 是一个顶层 const,需要明确指出。
- auto 会保留底层 const。
概括一下就是 auto 会忽略引用与顶层 const。
const int ci = 1, cr = ci;
auto b = ci; // b 是一个普通的 int。
auto c = cr; // c 是一个普通的 int。
const auto d = ci; // d 是一个 const int
auto &e = ci; // e 是一个常量引用(常量引用是底层 const)。注意这个微妙的地方。
auto f = &ci; // f 是一个 const int*(位于左边的 const 是底层 const)
int 与 int *、int & 是一个基本数据类型,而 const int 与 int 不是一种类型。
用 auto 定义引用时,必须用 & 指明要定义的是引用。
🍭2.5.3 decltype类型指示符
- 希望从表达式的类型推断出要定义的变量的类型。但是不需要进行初始化时,可以使用decltype。
- 如果如果 decltype 使用的表达式是一个变量,则它返回
该变量的类型(包括顶层 const 和引用在内)。
- decltype与auto不同,decltype不会忽略引用和顶层const
- 引用从来都是作为对象的别名出现,只有在 decltype 处是例外。
当获得的对象类型是引用时,必须初始化
const int ci = 0, &cj = ci;
decltype(ci) x = 0; // x 的类型是 const int
decltype(cj) y = x; // y 的类型是 const int&
decltype(cj) z; //错误 z 是一个引用,必须初始化
decltype和引用
- 如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型
- 如果表达式的内容是解引用操作,那么decltype将得到引用类型
- decltype((variable))---双层括号的结果永远是引用
int i = 6, &r = i, *p;
decltype(r+0) b; // b 的类型是 int,因为 r+0 的结果类型是 int。
decltype(*p) c = i; // c 的类型是 int&。
decltype((i)) d = i; // d 的类型是 int&。
🍮2.6 自定义数据结构
🍭2.6.1 定义Sales_data类型
struct+类名+类体+分号。类体可以为空。
struct Sales_data{}; //结尾注意有分号
对象定义有俩种方式
struct Sales_data{} students; //方式1
Sales_data students; //方式2
定义类时可以给数据成员提供类内初始值以进行初始化。没有类内初始值的成员则被默认初始化。
类内初始值可以放在花括号中或等号的右边,不能使用圆括号。
🍭2.6.2 使用Sales_data类(没东西,暂时省略hhh)
🍭2.6.3 编写自己的头文件
- 类通常定义在头文件中,类所在头文件的名字应与类的名字一样。
- 头文件通常定义那些只能被定义一次的实体,比如类、const、constexpr 等。
- 头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明。
预处理器概述
确保头文件多次包含仍能安全工作的常用技术是预处理器。
预处理变量有两种状态:已定义和未定义。一般把预处理变量的名字全部大写。
整个程序中的预处理变量包括头文件保护符必须唯一,通常基于头文件中类的名字来构建保护符的名字,以确保其唯一性。
c++ 中包含三个头文件保护符:
define:把一个名字设定为预处理变量
ifdef :当且仅当变量已定义时为真。
ifndef:当且仅当变量已定义时为真,一旦检查结果为真,则执行后续操作直到遇到 #endif 为止
endif
预处理变量无视作用域的规则,作用范围是文件内