第2章 变量和基本类型
1. 基本内置类型
1.1 算术类型
算术类型分为两类: 整型 (包括字符和布尔类型在内)和 浮点型
1.2 类型转换
当在程序中使用一种类型而其实对象应该取另一种类型时,程序会自动进行类型转换。
bool b = 42; // b 为真
int i = b; // i 的值为1
i = 3.14; // i 的值为3
double pi = i; // pi 的值为 3.0
unsigned char c = -1; // 假设 char 占8bit, c 的值为255
signed char c2 = 256; // 假设 char 占8bit, c2 的值是未定义的
含有无符号类型的表达式
无符号和有符号数混用带来的问题
示例1:
unsigned u = 10;
int i = -42;
std::cout << i + i << std::endl; // 输出 -84
// u 转换成无符号的数,其值为4294967254
std::cout << u + i << std::endl; // 如果 int 占 32bit,输出 4294967264
当一个算术表达式中既有无符号数又有 int
值时,那个 int
值就会转换成无符号数
unsigned u1 = 42, u2 = 10;
cout << u1 - u2 << endl;
cout << u2 - u1 << endl;
示例2:
for (int i = 10; i >= 0; --i)
std::cout << i << std::endl;
// 错误: 变量u永远不会小于0,循环条件一直成立
for (unsigned i = 10; i >= 0; --i)
std::cout << i << std::endl;
切勿混用带符号类型和无符号类型
1.3 字面值常量
一个形如 42 的值被称作字面值常量 , 每个字面值常量都对应一种数据类型,字面值常量的形式和值决定了它的数据类型。
整型和浮点型字面值
整型
20 // 十进制
024 // 八进制
0x14 // 十六进制
浮点型
3.14159 3.14159E0 0. 0e0 .001
字符和字符串字面值
'a' // 字符字面值
"Hello World!" // 字符串字面值
字符字面值的类型是 char
,字符串字面值是字符数组类型 const char[]
指定字面值的类型
我们可以通过下表的符号来指定字面值的类型
示例:
L'a' // 宽字符型字面值,类型是 wchar_t
u8"hi!" // utf-8 字符串字面值
42ULL // 无符号整型字面值,类型是 unsigned long long
1E-3F // 单精度浮点型字面值,类型是 float
3.14159L // 扩展精度浮点类型字面值,类型是 long double
布尔字面值和指针字面值
true
和 false
是布尔类型的字面值:
bool test = false;
nullptr
是指针字面值。
2. 变量
变量提供一个具名的、可供程序操作的存储空间。
### 2.1 变量定义
C++变量的定义要指定变量的类型。
//变量定义并初始化
int a = 2;
//变量定义
int b;
//变量赋值
b = 1;
在C++中变量的初始化和赋值是有区别的,
int a = 2;
的=
运算符表示的是初始化,而在
b=1;
中的=
是赋值。
C++11 列表初始化
int val = 1;
int val = {0};
int val{0};
int val(0);
使用 {}
来初始化变量,称为列表初始化,这种方式初始化变量,编译器化检查初始化的变量是否符号定义的变量的类型
long double pi = 3.14;
int a{pi}; //错误,编译器会检查类型,无法通过编译
总结:C++变量初始化的语法形式有三种:
=
,()
,{}
默认初始化 ,定义变量时没有初始化变量的值,则变量会被默认初始化。默认初始化的值取决于变量定义的类型。定义在函数体内的局部变量和类中的成员属性是不会被初始化的 (不同编译器的实现可能会不同), 所以不要试图使用任何方式去访问这些变量。
#include <iostream>
using namespace std;
int init;
struct A {
int m;
void print()
{
cout << m << endl;
}
};
void test()
{
int un_init;
cout << un_init << endl;
}
void test01()
{
A a;
a.print();
}
int main()
{
test01();
cout << init << endl;
return 0;
}
2.2 声明和定义
声明,使程序知道变量(对象)的存在
定义,负责创建于名字关联的实体
extern int i; //声明i
extern int i = 1; //错误,给i赋值extern失效
int j; //声明并定义j
变量能且只能被定义一次,但是可以被多次声明
extern
的使用参考:https://www.jianshu.com/p/165b3410b7fa
2.3标识符
变量命名按照规范,不要使用保留关键字。
命名规则,供参考:
- 普通的局部变量和函数参数名使用小驼峰(第一个单词首字母小写,其他单词首字母大写), 例:
userName
- 全局变量前加
g_
, 后面的按小驼峰规则 ,g_userName
- 静态变量前加
s_
, 后面按小驼峰规则,s_userName
- 类名使用大驼峰,所有单词的首字母大写 ,
UserManage
- 类属性(成员变量)前面加
m_
,后面按小驼峰规则 ,m_userName
- 常量全部使用大写,多个单词用
_
分割,MAX_NUMBER
2.4 名字的作用域
局部变量不能和全局的变量重名,嵌套的块,内部的不要和外部的重名。重名的变量采取就近原则。
3. 复合类型
复合类型的声明语句是由一个 基本数据类型 和紧随其后的一个 声明符 列表组成。
3.1 引用
引用 就是为变量(对象)起一个别名
int val = 1024;
int val1 = 102;
int& refVal = val; //refVal指向val
refVal = val1; //refVal引用并没有改变,只是改变了refVal指向的变量val的值,val = val1
int &refVal2; //错误,引用必须初始化
引用定义
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都是引用
// 类型必须一致
int &refVal4 = 10; // 错误: 引用类型的初始值必须是一个对象
double dval = 3.14;
int &refVal5 = dval; // 错误:此处引用类型的初始值必须是 int 型对象
注意:
- 引用必须初始化,且不能改变
&
符号可以紧靠基本类型(int), 也可以紧靠变量名- 引用绑定的对象类型必须和引用类型是一致的
以上说的引用都是左值引用,C++11还有右值引用
3.2 指针
指针 是 “指向” 另外一种类型的复合类型。指针和引用的不同,指针本身是一个对象,允许对指针赋值和拷贝;指针无须在定义时赋初值。
指针的定义,将声明符写成 *d
的形式,其中 d
是变量名,如果一条语句种定义类几个指针变量,每个变量前面都必须有符号 *
int *p1, *p2; // ip1和ip2都是指向int型对象的指针
double dp, *dp2; //dp2实质性double型对象的指针,dp是double型对象
指针使用建议:
- 指针定义是可以不初始化,但建议定义时初始化,如果没有想好指向哪个变量,可以初始化为空指针
- 操作指针时,须确定操作的不是空指针和野指针(无效指针)
获取对象的地址
指针存放某个对象的地址,要想获取该地址,需要使用 取地址符 (操作符&
):
int ival = 42;
int *p = &ival; // p 存放变量ival的地址,或者说p是指向变量ival的指针
指针类要和所指对象类型匹配
double dval;
double *pd = &dval;
double *pd2 = pd;
int *pi = pd; // 错误
pi = &dval; // 错误
引用不是对象,没有实际地址,所以不能定义指向引用的指针
指针值
指针值(即地址)应属于下列4种状态之一:
- 指向一个对象
- 指紧邻对象所占空间的下一个位置
- 空指针,意味着指针没有指向任何对象
- 无效指针,也就是上述情况之外的其他值
利用指针访问对象
如果指针指向一个对象,则允许使用 解引用符 (操作符*
)来访问对象
int ival = 42;
int *p = &ival;
cout << *p; // 由符号 * 得到指针p所指向的对象,输出 42
*p = 0; // 由符号 * 得到指针p所指向的对象,即可经由p为变量ival赋值
cout << *p; // 输出0
解引用操作仅使用与那些切实指向了某个对象的有效指针
空指针
空指针不指向任何对象,在操作指针之前必须确定为一个非空指针。下列几个生成空指针的方法
int *p1 = nullptr; // 等价于 int *p1 = 0;
int *p2 = 0; // 直接将 p2初始化为字面常量0
// 需要首先 #include <cstdlib>
int *p3 = NULL; // 等价于 int *p3 = 0;
赋值和指针
指针变量的赋值是指将一个地址值赋值给指针变量,从而指向一个新的对象
int i = 42;
int *pi = 0; // pi被初始化,但没有指向任何对象
int *pi2 = &i; // pi2 被初始化,存有i的地址
int *pi3; // 如果 pi3 定义于块内,则pi3的值是无法确定的
pi3 = pi2; // pi3和pi2指向同一个对象
pi2 = 0; // 现在pi2不执行任何对象
区分对指针指向对象的赋值
*pi = 0; // ival 的值被改变,指针pi并没有改变
其他指针操作
只要指针拥有一个合法值,就能将它用在条件表达式中。如果指针的值是 0, 条件取 false
,任何非0的指针对应的条件值都是 true
int ival = 1024;
int *pi = 0; // pi 合法,是一个空指针
int *pi2 = &ival; // pi2是一个合法的指针,存放着ival的地址
if (pi) // pi 的值是0,条件的值为false
// ...
if (pi2) // pi2 指向ival,它的值不为0, 条件的值是true
// ...
对于两个类型相同的合法指针,还可以用相等操作符 (==
)或不相等操作符 (!=
)来比较它们,比较的结果是布尔类型。
void* 指针
指针就是一个整数,没有实际的数值大小,只是一个编号,这个编号指向的是内存中的某个地址。指针无论定义成什么基本类型,其值都是一个固定位数的整数,指针类型数据的大小取决于系统的位数,32bit的系统指针变量大小是4byte = 32 bit, 64 bit系统指针是 8 byte = 64bit。
void*
是一种特殊的指针类型,可用于存放任意对象的地址。它所存放的地址就仅仅是内存空间的一个地址,因为没有指定具体对象的类型,所以无法用 void*
指针访问所指向的地址。
double obj = 3.14, *pd = &obj;
// 正确: void* 能存放任意类型对象的地址
void *pv = &obj; // obj 可以是任意类型的对象
pv = pd; // pv 可以存放任意类型的指针
定义指定类型的指针只是为了提供操作数据时需要操作的字节数。
例如,
int
型的指针,在使用指针改变指向的数据时,改变的是以该指针变量为首地址的4个字节内存,同样对
int
型指针的加或减的操作也是以4个字节为基本单位
3.3 理解复合类型的声明
int i = 42;
int *p; //p是int型的指针
int *&r = p; //r是一个对指针p的引用
r = &i; //r是一个指针引用,因此给r赋值&i就是令p指向i
*r = 0; //解引用r,就是解引用指针p,将p指向的变量i的值改为0
Tip: 面对一条比较复杂的指针或引用的声明语句时,从右向左读有助于弄清楚它的真实含义。
4. const 限定符
const 用于定义一个不能改变的变量, 所以定义时就必须初始化
cont int bufSize = 512; //用字面值常量初始化
cont int i = get_size(); //用函数返回值初始化, 运行时初始化
int j = 10;
cont int k = j; //用其他变量初始化
const
定义的变量只对本文件可见,要使其他文件也可见需使用extern
4.1 const的引用
const int ci = 1024;
const int &r1 = ci; //正确,引用r1和ci都是常量
r1 = 42; //错误, r1 是对常量的引用
int &r2 = ci; //错误,不能让一个常量引用指向一个常量对象
int i = 42;
cont int j = 10;
const int &r1 = i; //正确,允许将 const int&绑定到一个普通int对象
r1 = 10; //错误,不能通过常量引用改变i的值
const int &r2 = 42; //正确
const int &r3 = r1 * 2; //正确
int& r4 = j; //错误
int &r4 = r1 * 2; //错误
组合关系
int i |
cont int i |
|
---|---|---|
int &r |
✔ | ❌ |
cont int &r |
✔ | ✔ |
4.2 指针和const
拷贝关系:
int i |
cont int i |
|
---|---|---|
int *p |
✔ | ❌ |
cont int *p |
✔ | ✔ |
const指针
int errNumb = 0;
int *const curErr = &errNumb; //curErr将一直指向errNumb,不可以改变指向
const double pi = 3.14;
const double *const pip = π //pip是一个指向常量对象的常量指针
从右向左读
const 和指针
- 指向常量的指针 :指向的是一个常量,不能通过指针改变其所指的对象的值
- 常量指针 :指针变量本身为常量,必须初始化,一旦初始化完成,它的值(也就是存放在指针中的那个地址)就不能在改变了。
const
在 *
之后说明指针式一个常量,在 *
之前说明指向的是一个 const
对象
const double *const pip = π //pip是一个指向常量对象的常量指针
double const * const pip = π // 和上面等价,但不建议这样写
4. 3 顶层const
顶层const : 表示该变量(对象)本身是常量,不可以改变
底层const: 表示指向的变量(对象)是一个常量
int i = 0;
int *const p1 = &i; //p1是指针,p1的指向不能改变,顶层
const int ci = 42; //ci是普通变量,ci的值不能改变,顶层
const int *p2 = &ci; //p2是一个指针,它必须指向 const int型的数据,但是本身的指向可以改变,底层
const int *const p3 = p2; //第一个底层,第二个顶层
const int &r = ci; //用于声明引用的const都是底层
引用类型的变量自带顶层const 即引用一旦赋值(指向某个变量)就不可以在变化(指向另一个变量)
4.4 constexpr和常量表达式
C++11 用 constexpr
关键字声明一个变量,编译器会检查该表达式是否是一个常量表达式。
constexpr和指针
const int *p1 = nullptr;
constexpr int *p2 = nullptr;
int *const p3 = nullptr;
p2 和p3是等价的,
constexpr
修饰指针变量是被定义为顶层const
constexpr const int *pi = nullptr;
// 等价定义
const int *const pi = nullptr;
5. 处理类型
为了复杂程序更加易读易写,通常会给类型取别名,或是利用C++提供的特性自动推导复杂类型。
5.1 类型别名
typedef
传统的方法是使用 typedef
关键字定义类型别名
typedef double wages; //wages表示是double类型
typedef wages base, *p; //base = wages = double, p = double*
//数组的别名
typedef int arrT[10]; //arrT是一个类型别名,他表示的类型是含有10个整数的数组
using arrT = int[10]; //和上面的等价
using
C++11 提供了一种新的方式,使用 using
using SI = Sales_item; //Sales_item是一个类类型, SI表示是该类的别名
这里的
using
要和using namespace std;
中的using
区分开。后者是表示引入命名空间,类似于java和python的导包操作
指针、常量的类型别名
typedef char* pstring;
const pstring cstr = 0; //char *const cstr = 0;
const pstring *ps; //char **const ps;
第二行的定义不能理解成 const char *cstr = 0;
const pstring
中const
是对pstring
的修饰,而pstring
是一个char*
类型,因此const petring
是指向 char的 [常量指针](#const point) ,而并不是指向常量字符的指针
5.2 auto 类型说明符
C++11 auto
类型说明符可以让编译器分析表达式所属的类型。
int val1 = 1, val2 = 3;
auto val = val1 + val2; //编译器可以自动推出val为int类型
auto i = 0, *p = &i; //正确,编译器通过字面值推出i为int,p为int*
auto sz = 0,pi = 3.14; //错误,编译器无法推出类型, sz, pi类型不一致无法统一
复合类型、常量和auto
-
当使用引用类型推导类型是,
auto
推导的类型是引用指向变量的实际类型int i = 0, &r = i; auto a = r; // r是int型的引用,因此a是int型
-
auto
会忽略掉顶层const, 同时底层const则会保留下来const int ci = i, &cr = ci; auto b = ci; //int b = ci; auto c = cr; //int c = cr; cr是ci的别名,ci本身是一个顶层const auto d = &i; //int *d = &i; auto e = &ci; //const int *e = &ci; 对常量对象取地址是一种底层const
如果希望推断出的auto类型是一个顶层const,需要明确指出:
const auto f = ci; // const int f = ci;
指定引用类型
auto &g = ci; //const int &g = ci; auto &h = 42; //错误,int &h = 42; const auto &j = 42; //const int &j = 42;
auto 使用建议:
使用auto声明变量一定要做到心里有数,你知道编译器会推断出的什么样的类型
通常使用auto是对于一些类型名比较复杂的变量,使用auto写起来更方便
5.3 decltype 类型指示符
C++11 decltype
可以不执行表达式,编译器自动推断出表达式的返回值类型
decltype(f()) sum = x; //sum的类型和f()的返回类型一样
通过 f()
推断出返回类型,但是并不会执行 f()
decltype 和const
decltype
处理顶层 consth和引用的方式和auto有点不同,如果 decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内)
const int ci = 0, &cj = ci;
decltype(ci) x = 0; //const int x = 0
decltype(cj) y = 0; //const int &y = 0;
decltype(cj) z; //错误, const int &z; 引用必须初始化
decltype 和引用
int i = 42, *p = &i, &r = i;
decltype(r + 0) b; //int b;
decltype(*p) c; //错误, int &c; 引用需要初始化
r
是引用decltype(r)
是引用,但是r + 0
是一个int型数据解引用指针得到的是指针所指的对象,,因此
decltype(*p)
是int&
变量加上 ()
得到的是引用类型
decltype((i)) d; //错误, int& d; 引用类型需要初始化
decltype(i) e; // int e;
decltype((variable))
的结果永远是引用
6. 自定义数据结构
这里的自定义数据结构就是指类类型的数据,在C++中定义类的关键字有class
和 struct
class ClassName{
//属性
//方法
};
struct ClassName{
//属性
//方法
};
class
和struct
在功能上是完全一样的,两者唯一的不同是默认的权限不同
class
默认的权限是私有的(private), 而struct
是公有的(public)
注意:c语言中的结构体是不能有方法(函数)
关于类更具体的介绍在后面的章节