内容:
- 基本内置类型
- 变量
- 复合类型:引用、指针
- const限定符:const的引用、指针和const、顶层const、constexpr和常量表达式
- 处理类型:auto、decltype
资料来源:《C++ Primer》、《C++ Primer plus》、《Effective C++》、随手博客摘要。
1、算数类型
C++:算数类型 | ||
---|---|---|
类型 | 含义 | 最小尺寸 |
bool | 布尔类型 | 未定义 |
char | 字符 | 8位 |
wchar_t | 宽字符 | 16位 |
char16_t | Unicode字符 | 16位 |
char32_t | Unicode字符 | 16位 |
short | 短整型 | 16位 |
int | 整型 | 16位 |
long | 长整型 | 32位 |
long long | 长整型 | 64位 |
float | 单精度浮点数 | 6位有效数字 |
double | 双精度浮点数 | 10位有效数字 |
long double | 扩展精度浮点数 | 10位有效数字 |
注意
-
带符号类型(signed)和无符号类型(unsigned):int、short、long、long
long都是带符号,在其前面添加unsigned即可的到无符号类型,无符号仅表示大于等于0的数。 -
字符型被分为三类,char和signed char类型不一样,8比特signed char理论上表示为-127至127区间内的值(大多数现代计算机为-128至127),8比特unsigned char则表示为0-255。
了解climits头文件
#include<iostream>
#include<climits>
#include<locale>
using namespace std;
int main()
{
//wchar_t为宽字符,常用在国际程序,可以避免文件在不同平台上出现乱码的情况
setlocale(LC_ALL, "chs"); //使用setlocale函数将本机的语言设置为中文简体
wchar_t wt[] = L"中国"; //LC_ALL表示设置所有的选项(包括金融货币、小数点,时间日期格式、语言字符串的使用习惯等),chs表示中文简体
int n_int = INT_MAX; // INT_MAX:在climits头文件当中定义的预处理方式,表示最大数值
short n_short = SHRT_MAX;
long n_long = LONG_MAX;
long long n_llong = LLONG_MAX;
cout << "wchar_t is " << sizeof (wchar_t) << " bytes." << endl; //所占字节
cout << "int is " << sizeof (int) << " bytes." << endl;
cout << "short is " << sizeof n_short << " bytes." << endl;
cout << "long is " << sizeof n_long << " bytes." << endl;
cout << "long long is " << sizeof n_llong << " bytes.\n" << endl;
cout << "Maximum values: " << endl; //最大数值
wcout << "wchar_t: " << wt << endl; //wchar_t类型需要用wcout输出,不然只能输出地址
cout << "int: " << n_int << endl;
cout << "short: " << n_short << endl;
cout << "long: " << n_long << endl;
cout << "long long: " << n_llong <<"\n"<< endl;
cout << "Minimum int value = " << INT_MIN << endl; //int最小值
cout << "Bits per byte = " << CHAR_BIT <<"\n"<< endl; //字节占位
system("pause");
return 0;
}
运行结果:
1.1命名规则
- 名称中只能使用字母字符、数字和下划线(_)
- 名称的第一个字符不能为数字
- 区分大小写字符
- 不能用C++关键字作名称
- 尽量不要使用双下划线(如_time_stop)命名,编译执行可能会导致不确定性,不出现编译错误原因,但做法不非法
- C++对名称长度无限制,名称所有字符都有意义(尽量使用英文或易懂的缩写命名,可读性增加),但有些平台有长度限制。
建议:如何选择类型
- 当明确知晓数值不可能为负时,选用无符号类型。
- 使用int执行整数运算。short太小,而long和int有一样的大小,超过int就使用long long。
- 算数表达式不要使用char或bool,只有在存放字符或布尔值时才使用它们。算数表达式用char会很容易出现错误(符号问题)。如果必须使用,那么就明确指定类型为signed char或者unsigned char。
- 执行浮点数运算选用double,因为float精度不够而且双精度运算代价相差不多。对于某些机器来说双精度运算速度比单精度快。
1.2 类型转换
定义包含数据和能参与的运算,类型转换顾名思义就是将对象从一种类型转换成另一种类型。
//按值传递,提供了它的数的副本,也就是拷贝
bool b = 42; // b 为真(true)
int i = b; // i 的值为1
i = 3.14; // i 的值为3
double pi = i; // pi 的值为3.0,近似处理
unsigned char c = -1; //假设 char 占8比特,c的值为255 (0至255),超出范围时,结果是无符号类型表示数值总数取模后的余数。
signed char c2 = 256; //假设char占8比特,c2的值是未定义的 (-128至127),给带符号类型传递超出表示范围的值时,结果是未定义的,程序可能继续工作、崩溃、生成垃圾数据。
/*int类型数据所占内存空间为32位。
其中有符号整型变量取值范围为-2147483648~2147483647,
无符号型整型变量取值范围为0~4294967295U.
*/
unsigned u = 10;
int i = -42;
std::cout<< i + i << std::endl; //输出-84
std::cout<< u + i <<std::endl; //如果int占32位,输出的值为4294967264。一般我们自己计算机环境中的int占位是32位,所以u+i=-32,再将负数加上无符号数的值再取模则为(2^32-32)=4294967264, 2^32=4294967296。
2、变量
- 变量提供一个具名的、可供程序操作的存储空间。
- 每个变量都有其数据结构,决定变量所占内存空间大小和布局方式、该空间嫩存储的值得范围,以及变量能参与的运算
- 变量和对象可以互换使用。对象是指一块能存储数据并具有某种类型的内存空间。
2.1初始值
//定义并赋值,随后初始化b
double a=1,b=a*2;
//调用函数d,用函数返回值初始化c
double c=d(a,b)
- 初始化不是赋值,初始化是创建变量给定一个初始值
- 赋值是把当前的值擦除,用新值代替
2.2变量声明和定义的关系
extern int i; //声明i,未定义i
int j; //声明并定义j
extern double pi=3.1416; //定义pi
- 声明是规定类型和名字
- 定义除了声明的功能,还申请了存储空间,增加了对内存的使用
- 声明多用在分离式编译,比如头文件跟函数的书写上
3、复合类型
- 引用、指针
- 处理类型:类型别名、auto类型说明符、decltype类型说明符
3.1.1 引用
- 接下来介绍的引用都是左值引用,右值引用在后面的类部分介绍
- 引用即别名,引用并不是一个对象,而是给一个已经存在的对象起另外一个名字。定义引用时,程序会把引用和它的初始值绑定在一起
- 引用只能定义一次,之后不能绑定其他对象了(一次性),只能改变对象的值,且不能为空,使用时不需要解引用(*)
- 引用不需要分配内存空间,直接使用被引用对象的内存
int i = 1024;
int &j = i; //allowed
int &i2; //not allowed,uninitialized
- 引用必须要与对象使用相同类型,但是有两个例外可以使用,一个是可被类型转换的常量(第二个例外是在派生类向基类的引用中会用到),例如:
double j = 20;
const int &i=j; //i是j的别名
上面的过程是允许的,为了了解这个过程我们可以看一下编译器是怎么使他们成功的:
const int temp = j; //先让双精度的j生成一个临时的整型常量
const int &i = j; //让i绑定这个临时量
以下为错误例子:
int i = 20;
double &j = i; //错误:引用必须使用相同类型
double &j = 10; //错误:引用类型的初始值必须是一个对象
3.1.2 指针
- 与引用类似,指针可以实现对其他对象的间接访问
- 指针是一个实体是一个对象,允许对指针的赋值和拷贝,可以指向几个不同的对象
- 指针的赋值需要理解内存对野指针的理解,编程不像人,如果让一个指针指向数字,而不是内存地址,编译就会失败,但是使用类型转换指向一个内存就不一样了
- 野指针会指向一个已删除的对象或者访问受限的内存区域,与空指针不一样会明确NULL,不指向任何内存,避免程序出现崩溃
- 必须必须在指针解引用之前,将指针指向一个明确的、适当的地址
long *p3 = (long *)352; //表示p3指向编号为352的内存.如果是在保护模式下,这里不允许放入数据.
long *p3 =new long; *p=352; //表示在堆上存储整数352
long* p3; *p3 =352; //错误.往一个非法随机内存处存入数据.不过编译能过.
- 指针在定义时可以不赋初值,但会拥有一个不确定的值
- 当编译器访问未初始化的指针时,会出现无法预计的错误,所以使用指针时需要初始化全部指针
指针的基本用法:
1. 获取对象的地址
int i = 2;
int *p = &i; //在表达式的右值当中,&是取地址符
如果想让指针指向引用,是不正确的,因为引用不是对象,没有实际地址,所以不能定义指向引用的指针
2. 利用指针访问对象
int i = 2;
int *p = &i; //在表达式的右值当中,&是取地址符
cout<<*p; //输出2,当想要访问指针指向的对象时,需要用(*)解引用访问所指对象i
cout<<p; //输出地址号
*p = 0; //给解引用的指针赋值,就是给指向的对象i赋值,即i=0
cout<<*p; //输出0
* 解引用操作仅适用于那些确实指向某个对象的有效指针
3. 空指针
空指针不指向任何对象,在用到cstdlib头文件时,使用预处理变量NULL来给指针赋值时,指针值为0。
建议初始化所有指针,可以用对象、数值、nullptr或者NULL等初始化。如果访问了未初始化的指针,会发生意想不到的错误。
int *p1 = nullptr; //等价于int *p = 0;
int *p2 = 0; //直接初始化为字面常量0
int *p3 = NULL; //需要先添加头文件cstdlib,等价于int *p3 = 0; 推荐使用新标准当中的nullptr,尽量避免使用NULL
int *p;
int zero = 0;
p = zero; //错误:不能把int变量直接赋给指针,int *不能转换为int
p = &zero;
4. 赋值和指针
- 给指针赋值,就是令它存放一个新的地址
- 从而指向一个新的对象 而给指针赋值的任务则交给&
- 也可以用new运算符返回未命名的内存的地址,并且用delete释放内存,但是仅限于用new寻找地址的指针,并且对空指针也是有效的,可以提高内存的利用率,减少内存泄漏的危险
double *pn; //如果pn定义在块内,则pn的值无法确定,是危险的指针
double *pa;
char *pc;
double b=3.2;
pn=&b; //pn的值被改变,pn指向了b
pc=new char; //寻找一个适合的内存地址赋给pc
delete pc; //释放内存
pa=new double;
pa=pn; //pa与pn共同指向b
*pa=0; //指针pa指向b,解引用后把b的值设置为了0,但是指针pa不变
5.其他指针操作
只要指针是存在的、有意义的,就可以用在条件表达式中。合法的指针(不为零或地址安全),条件为true。指针值为0则为false。
int i = 1024;
int *pi = 0;
int *pi2 = &i;
if(pi) //pi值为0,false
if(pi2) //pi2指向i,true
6.void(*) 指针
void指针可用于存放任意对象的地址,遗憾的是我们不能直接操作void*指针所指的对象,因为并不知道指针指向的对象是什么类型,就无法确定在对象上做哪些操作,在类方面会更加详细的了解这个特殊的指针
double obj = 3.14;
double*pd = &obj;
void *pv = &obj;
pv = pd;
关键概念:(&)与(*),既能用作表达式里的运算符,也能作为声明的一部分。主要区分在于等号左右值以及类型位置的区分
int i = 2;
int &r = i; //由于在类型名后出现,则是声明r是一个引用
int *p; //由于是类型名后出现,则是声明*是一个指针
p = &i; //由于在表达式中出现,则是取地址符,等价于*p=i;
*p = i; //由于在表达式中出现,则是解引用符,未初始化会出错
int &r2 = *p; //&为引用,*为解引用
4、const限定符
- 可使变量定义后,变量的值不被改变,const修饰的变量必须先初始化
- 但是在内置和POD类型内,可以不初始化,这个总结会在往后
- 比起用宏定义#define,我们更趋向于使用const,因为可以减少编译时间
- #define是字符替换,没有类型区别,没有类型安全检查,宏定义后的数据不属于变量,容易出现边际效应等错误
#define N 2+3;
我们预想的N值是5
int a=N/2;
我们预想的a的值是2,可实际上a的值是3
原因在于在预处理阶段,
编译器将a=N/2处理成了a=2+3/2;
这就是宏定义的字符串替换的“边缘效应”因此要如下定义:#defineN(2+3)
用const定义表达式没有上述问题。
- 被const修饰的变量,会变成只读型的变量,会在编译过程进行安全检查
- const常量需要进行内存分配,存储与程序的数据段中。而宏定义并不需要占用内存,存储在代码段当中,需要时直接替换目标对象的数据。
const 类名 对象名
const int a = 10; //allowed
类名 const 对象名
int const a = 10; //allowed
int b = a; //拷贝允许的,拷贝的对象并不会被改变
const int a; //not allowed,uninitialized
4.1 const的引用
- 把引用绑定到const对象上,就是常量引用
- 被常量引用绑定的对象,不能通过常量引用修改其对象
- 引用不能指向常量,因为引用改变了常量自身的内容,为const提供了新的别名,但是const不允许这种修改
const int i =1024;
const int &j = i;
j=42; //not j是常量,不允许修改
int &j2 = i; //试图让非常量引用j2指向常量i,增加常量的变量别名
4.1.1初始化和对const的引用
int i = 42;
const int &j1 = i;
const int &j2 = 42;
const int &j3 = j1 * 2;
int &j4 = j1 * 2; //错误,常量引用不允许绑定到非常量引用上
1、 int常量引用可绑定到double非常量引用上,这个例子在引用上已经提及
double a = 4.33;
const int &b = a; //编译器生成了temp临时常量等于4.33,让b绑定在了temp上
2、非常量被常量引用绑定,可以依靠其他途径修改非常量
被const常量绑定的非常量对象,可以绑定非常量引用,并且依靠非常量引用修改非常量对象的值
int i = 42;
int &r1 = i;
const int &r2 = i; //i有了r1,r2的别名
r1 = 0; //可以利用非常量引用修改i的值
r2 = 0; //不可以利用常量引用修改i的值
4.2 指针和const
在了解指针和const的关系时,我们必须理解指向常量的指针、常量指针和指向常量的常量指针是什么意思
int j = 42;
const int *i = &j; //指向常量的指针,可以更换指向的对象,但是不能通过指针改变指向的对象的值
int *const i = &j; //常量指针,也可说是常指针,永远指向j,但可以通过指针修改指向的对象的值
const int *const i = &j; //指向常量的常量指针,不可修改指针指向,并不可通过指针修改指向的对象的值
- 普通指针不可以指向常量
- 指向常量的指针指向常量后,不可以依靠指针改变常量
- 指向常量的指针可以指向非常量对象,前提是类型一致
const double pi = 3.14;
double *ptr = π //not ptr是普通指针
const double *cptr = π
*cptr = 42; //常量指针不能修改常量
int j = 42;
const int *j = &j; //指向常量的指针可以指向普通变量,但是不可以通过指向常量的指针修改对象的值
int j = 2; //但可以依靠别的方法修改变量
4.2.1 const指针
- 常量指针必须初始化
- 初始化后指针的值就不能被改变,也就是不能依靠指针去修改指向的对象的值
- 但是将*放在const前面就另当别论了,*const表示的意思是不变的是指针本身,可以修改对象的值
const 类型名 *const 对象名,这个的含义是指向不能变,并且不能通过此指针改变对象的值
int i = 42;
int *const j = &i; //j将永远指向i
*j = 2; //允许修改i的值
int p = 1;
int *const j = &p; //不允许修改指向的对象
const int *const ptr = &p //永远指向p,并且不允许通过指针修改指向的对象的值
*ptr = 2; //错误,ptr是一个指向常量的常量指针
4.2.2 顶层、底层const
- 顶层const:表示指针本身是个常量,例如int *const i。永远指向初始化的对象,但是指针可以修改指向的对象的值
- 底层const:表示指针所指的对象是一个常量const int *i。可以改变指向,但是指针不可以修改指向的对象的值
- 简单的来说,靠*右边的const为顶层,靠左边的为底层
- 过多的描述没什么意义,主要在指向方面,需要看清楚是顶层还是底层,类型也要一致,除非他们类型可以转换
4.2.3 常量表达式和constexpr
常量表达式:指值不会改变并且能在编译过程得出结果的表达式
const int max = 20; //是常量表达式
const int num = max+1; //是常量表达式
int size = 2; //非常量表达式
const int s = get_size(); //非常量表达式,虽然s是常量,但是必须要等到运行才可以得出结果
constexpr变量:c++11标准规定,允许将变量声明为constexpr类型分辨一个初始值是不是常量表达式,而constexpr类型的内容,就是定义一个const,并且增加了编译器验证变量是否为常量表达式的功能
constexpr int i = 42; //是常量表达式
constexpr int j = i+1; //是常量表达式
constexpr int t = size(); //当size函数是一个constexpr函数时是一个常量表达式
指针和constexpr的关系:如果在constexpr的声明里定义了一个指针,则constexpr仅对指针有效,与指针所指的对象无关,也就是设置了个顶层const
constexpr int *p = nullptr; //p是一个指向整数的常量指针,顶层const
const int *q = nullptr; //q是一个指向整型常量的指针,底层const
const int i = 42;
constexpr const int *d = &i; //d是一个指向整数常量的常量指针
要注意的是,使用constexpr
5、处理类型
在后面的学习中,类型往往很难记,而且容易记错,这就需要用到一些工具处理了。
5.1.1 类型别名
- 类型别名是个名字,它是某种类型的名字(同义词)
- 可以使复杂的类型名字变的简单明了、易于理解和使用
- 定义类型别名有两种方法,一种是使用关键字typedef,另一种是新标准规定的方法,使用别名声明关键字using
typedef double a; //a设置为double的同义词
typedef a b,*p; //b是double的同义词,*p是double *的同义词
//MyInVector是std::vector<int,MyAlloc<int>>类型的同义词
using MyIntVector = std::vector<int, MyAlloc<int>>;
5.1.2 指针、常量和类型别名
typedef char*陷阱:const与typedef一起出现时,typedef不是简单的字符串替换
在下面的例子中有个需要注意的点,当const跟cptr共用时,他的含义是char * const,而不是我们想当然的const char *
,原因在于const给予了整个指针本身以常量性,也就是形成了常量指针char* const。
简单来说,记住当const和typedef一起出现时,typedef不会是简单的字符串替换就行
typedef char *cptr; //cptr是char *的同义词
const cptr cp = 0; //cp是指向char的常量指针,等价于char *const cp = 0;
const cptr *p; //p是一个指向char的常量指针,等价于char *const *p;
5.2 auto类型说明符
- auto的作用是代替我们去自动识别表达式的类型
- 它是通过识别初始值来判断类型的,所以使用auto时必须初始化
- 在同一条定义里面定义多个变量时,他们的类型要相同
auto i = 4;
auto l = 2;
auto j = i + l; //j初始化为i与l的和
auto i = 0,*p = &i; //正确,i是整数,p是整型指针
auto s = 0, pi = 3.14; //错误,s与pi的类型不一致
5.2.1 复合类型、常量和auto
- 编译器推断出来的auto类型有时候与初始值的类型不一样,编译器会适当地改变结果类型使其更符合初始化规则,比如auto一般会忽略顶层const,而底层const会被保留下来
- 在auto当中如果将等式右边取地址符,其实就是指针类型,因为一个值的地址,就是指向这个类型的值的指针
int i = 0,&r = i;
auto a = r; //a是一个整数类型,r是i的别名,i是一个整数
const int ci = i,&cr = ci;
auto b = ci; //b是一个整数(ci的顶层特性被忽略)
auto c = cr; //c是一个整数(cr是ci的别名,ci的顶层特性被忽略)
auto d = &i; //d是一个整型指针(i是普通整型,整型取地址就是指向整数的指针)
auto e = &ci; //e是一个指向整数常量的指针(ci是一个常量整数,对常量对象取地址是一种底层const)
- 如果希望auto类型是一个顶层const,则需要明确指出:
const auto f = ci; //ci的顶层特性被忽略,识别为int ci,f即为const int
- 当auto一个引用时,就不会出现上面的现象
auto &g = ci; //g是一个整形常量引用,绑定ci,g为ci别名
auto &h = 42; //错误,不能为非常量引用绑定字面值
const auto &j = 42; //正确
- 要在一条语句中定义多个变量,符号&和*知识属于声明符,并不是数据类型的一部分,初始值需要同一类
auto k = ci,&l = i; //k是一个整数,l是一个整型引用
auto &m = ci,*p = &ci; //m是对整型常量的引用,p是一个指向整型常量的指针
auto &n = i,*p = &ci; //错误,同一条定义语句,i是int,而&ci是const int,不能放在同一条auto中
5.2.2 decltype类型指示符
- 当不想初始化变量,又希望从表达式的类型推断出要定义的变量的类型,c++11新标准引入了新的类型说明符decltype
- decltype的作用是选择并返回操作数的数据类型,编译器会分析表达式并得到类型,却不去计算其实际值
- decltype处理顶层const和引用的方式与auto有些许不同,比如不会忽视顶层const
decltype (f()) sum = x; //sum的类型就是f函数的返回类型
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; //错误,引用必须初始化
- decltype的括号里是非引用但在外面再加一层括号,则为引用:decltype((d)),d为int,推断后为int&
int i = 1;
decltype(i) a; //正确,a为int
decltype((i)) b; //错误,b为引用必须初始化
- 当表达式不为变量,则decltype返回表达式结果对应的类型
int i = 42;
int *P = &i; //p指向i
int &r=i; //r是i的别名
decltype(r+1) b; //正确,由于括号内不为变量,返回他们表达式结果的类型,b为int
decltype(*p) c; //错误,*为解引用符号不为变量,解引用后c为int&,必须初始化
特别要注意的是,如果双括号,则结果永远为引用。单括号只有里面的值为引用,结果才为引用。
小结:
这些内容大部分都来自《C++ Primer》,总体而言还算简单易懂,因为这些都是基础知识,在往后的函数还有容器等内容中,这些知识是必不可少的,学习过程中也会进行一些扩展知识的填充,写总结的用意是为了能够增强对这些内容的印象,偶尔翻一下查看以前写的渣渣博客有什么错误,并且对其加以改正或者补充。