数据类型告诉我们数据的意义以及我们能在数据上执行的操作
2.1基本内置类型
基本数据类型:算术类型(字符,整型数,布尔值,浮点数)和空类型
2.1.1算术类型
- 两类:整数和浮点数
- C++标准规定
- 一个int至少和一个short一样大
- 一个long至少和一个int一样大
- 一个long long(C++11中新定义的) 至少和一个long一样大
- 一个char的大小至少和一个机器字节一样
- wchar_t用于确保可以存放机器最大扩展字符集的任意一个字符
- char16_t和char32_t则为Unicode字符集服务
带符号和无符号类型
- 除去布尔型和扩展的字符型外,其他整型可以划分为带符号的和不带符号的(表示大于等于0)
- 与其他整型不同,字符型被分为三种:char,signed char,unsigmed char.尽管有三种但是表现形式只有2中:带符号的和无符号的.char会表现为上述两种类型种的一种,具体要看编译器的设置.
- char和signed char并不一样.
如何选择类型
- 数值不可能为负数:无符号类型
- 使用int执行整数运算,如果超过了int的表示范围使用long long
- 在算数表达式中不要使用char和bool
- 如果你需要使用一个不大的整数,那么明确指定他的类型signed char或者unsigned char
- 执行浮点运算选用double
2.1.2类型转换
- 当我们给无符号类型一个超出他表示范围的值时,结果是初始值对无符号类型表示的数值总数取模后的余数.
- 当我们赋给带符号类型一个超出他表示范围的值时,结果是未定义的.
含有无符号类型的表达式
- 当一个表达式中既有无符号数又有int,那个int值就会转化为无符号数
int main()
{
unsigned u = 10;
int i = -42;
cout << i + i << endl; // -84
cout << i + u << endl; // 4294967264
return 0;
}
- 当从无符号数中减去一个值时,不管这个值
2.2 变量
2.2.1变量定义
string
是一种表示可变长字符序列的数据类型对象
是具有某种数据类型的内存空间
初始值
初始化
不是赋值
,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除而以一个新值来替代.
列表初始化
- 初始化的4中方式:
int units_sold = 0;
int units_sold = { 0 };
int units_sold{ 0 }; //列表初始化
int units_sold(0);
- 如果使用列表初始化且初始值存在丢失信息的风险,编译器会报错.
long double ld = 3.1415926212;
int a{ ld }, b = { ld }; //报错: 从“long double”转换到“int”需要收缩转换
int c(ld), d = ld; //警告:“初始化”: 从“long double”转换到“int”,可能丢失数据
默认初始化(default initalized)
- 默认值是什么有
变量类型
和定义变量的位置
决定 - 内置类型未被显示初始化,它的值有定义的
位置
决定,函数体之外
的内置类型初始化为0,函数体内
如果试图拷贝或者其他形式访问此类值将引发错误.
int mainExtern;
int main()
{
int intern;
//cout <<"main内部: " << intern << endl; //使用了未初始化的局部变量“intern”
cout << "main外部: " << mainExtern << endl; // 0
return 0;
}
- 每个类 各自决定其初始化对象的方式,而且是否容许不初始化就定义对象也由类自己决定.(这里在类中如何控制:通过设置构造函数的形参类决定,例如只定义有参构造函数,那么就不容许不初始化就定义对象的行为)
class Demo1 {
private:
int a;
};
int main()
{
Demo1 d1; //可以
return 0;
}
class Demo1 {
Demo1(int b):a(b) {};
private:
int a;
};
int main()
{
Demo1 d1; // 不存在默认构造函数
return 0;
}
string类规定如果没有指定初值则生成一个空串
2.2.1课后题
double a = b = 11.1; //未定义标识符b
这个错误应该这样理解。在C++中,=是从右到左进行结合,那么这个表达式可以拆分如下
double salary = (wage = 9999.99);
这样的话,这条表达式的意思是将9999.99赋值给wage这个变量,然后用wage这个变量来初始化这个salary变量。由于在赋值操作时,编译器未找到wage这个变量,那么就会报错。
将表达式拆分如下,即可通过编译:
double wage;
double salary = wage = 9999.99;
参考:https://www.cnblogs.com/dn96/p/9817928.html
编译器并未被要求检查未初始化的变量,建议初始化每一个内置类型的变量
2.2.2变量声明与定义的关系
- c++支持分离式编译,容许将程序分割为若干个文件,每个文件可被独立编译.
声明
:名字为程序所知道,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明.定义
:负责创建与名字关联的实体.声明而非定义
:如果想声明一个变量而非定义它,则在变量前面加上关键字extern
,而且不要显式的初始化变量
extern int i; //声明i而非定义i
int j; //声明并定义i
- 任何包含显式初始化的声明成为定义.
extern double pi = 3.12 //定义
- 如果试图在函数体内部使用extern进行变量变量初始化,会引发错误.
int main()
{
extern int mainExtern = 1; // error: 不能对带有块范围的外部变量进行初始化
return 0;
}
函数体内部可以声明并且使用extern标记的变量,但是如果在函数体内定义一个extern变量,那么由于函数体内变量作用域仅限于函数体内的原因,将会导致出错。
变量能且只能被定义一次,但是可以被多次声明
课后题
extern int ix = 1024; //定义了变量ix
int iy; // 声明并且定义了变量iy
extern int iz; //声明了变量iz
C++是一种静态类型语言,在编译阶段检查类型
2.2.3标识符
2.2.4名字的作用域
当你第一次使用变量时再定义它,即在对象第一次被使用的地方附近定义它是一种好的选择
2.3 复合类型
本节介绍2种
- 引用
- 指针
2.3.1引用
在C++11中增加了所谓的右值引用,此种引用主要用于内置类.当我们使用术语引用时特指左值引用
- 一般在初始化变量时,初始值会被拷贝到新建的对象中.然而在定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用.
引用即别名
引用并非对象,相反的,它只是一个为已经存在的对象所起的另外一个名字
- 由于引用本身并非一个对象,所以不能定义引用的引用
引用的定义
- 除了两个例外情况,其他所有引用的类型必须都要和与之绑定的对象严格匹配
- 引用只能绑定在对象上,而不能与字面值或者某个表达式的计算结果绑定在一起.
2个例外情况
55页的2.4.1和534页的15.2.3
- 例外情况一:在初始化常量引用时,容许用任意表达式作为初始值,只要该表达式的值可以转化为引用的类型即可
int i =42;
const int &r1 = i; // 容许const int& 绑定到一个普通的int对象上
const int &r2 = 42; //正确,r1是一个常量引用
const int &r3 = r1*2 //正确,r3是一个常量引用
int &r4 = r1*2; //error,r4是一个普通的非常量引用
如何理解这种例外:
例如如下:
double dval = 3.14;
const int &ri = dval;
编译器做了如下优化:
double dval = 3.14;
const int tmp = dval;
const int & ri = tmp;
产生一个tmp临时变量
,即ri绑定到一个临时变量上.
如果ri不是常量时,那么可以通过修改ri来修改临时变量,这很荒谬.
- 常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是常量未做限定
int main()
{
int i = 42;
int& r1 = i;
const int& r2 = i;
r1 = 0;
cout << "i = " << i << endl; //0
cout << "int &r1 = " << r1 << endl; //0
cout << "const int & r2 = " << r2 << endl; //0
}
2.3.1课后题
int ival = 1.01;//合法
int &rval = 1.01;//非法
/*
引用必须指向一个实际存在的对象而非字面值常量
*/
int &rval2 = ival; //合法
2.3.2指针
指针VS引用
- 指针本身是一个对象,容许对指针进行拷贝和赋值,可以在其生命周期中先后指向几个不同的对象
- 指针不需要在定义时赋初始值
获取对象的地址
-
引用不是对象,没有实际地址,所以不能定义指向引用的指针
-
除了两种例外情况,所有指针的类型必须和它所指向的对象严格匹配
两种例外情况
56也的2.4.2和15.2.3
例外情况一:
- 允许令一个指向常量的指针指向一个非常量对象
const double pi = 3.14
const double *cptr = π //指向常量的指针
double dval = 3.14;
cptr = &dval;//正确
常量引用和指向常量的指针都没有规定所指的对象必须是一个常量
指针值
指针的值应属下列4种状态之一
- 指向一个对象
- 指向紧邻对象所占空间的下一个位置
- 空指针,没有指向任何对象
- 无效指针,也就是上述情况的其他值
试图拷贝无效指针或者以其他方式访问无效指针的值都将引发错误,这里和使用未经初始化的变量是一样的.
对于第2种情形的理解:
其实就是指向一个对象的后一位置,这个位置是不存在对象的,对此指针解引用将会是未定义的结果。这种指针主要作用是用来标识有效对象的边界,一旦达到这里就表示要有效对象要结束了,当前已超界。对于顺序容器和数组这种连续存储对象的类型比较有用。有了这个指针我就可以知道有效对象在内存中占据哪一段内存单元了。
利用指针访问对象
解引用仅适用于哪些确实指向了某个对象的有效指针
空指针
int *p1 = nullptr; // == int *p1 = 0; == int *p1=NULL;
- NULL包含在头文件
cstdlib
中,它的值就是0.称为预处理变量 - nullptr为字面值
- 预处理是运行于编译之前的一段程序.预处理变量不属于命名空间std.
int zero = 0;
int *pi = zero; //error
int *p1= 0; //正确
初始化所有指针
赋值和指针
- 指针和引用都能提供对其他对象的间接访问,然而引用本身并非对象,一旦定义,就无法令其在绑定到其他对象上.
其他指针操作
- 一个指针指向某对象,同时另一个指针指向另外对象的下一个地址,此时也有可能出现这两个指针指相同的情况,即指针相等 为什么
另外对象和某对象正好相邻,另外对象的下一对象正好是某对象。
void*指针
void*
可用于存放任意对象的地址- 不能直接操作
void*
指针指向的对象,因为不知道其类型和在其上的操作 - 以
void*
的视角来看内存空间也就仅仅是内存空间没办法访问内存空所存储的对象
2.2.3理解复合类型的声明
指向指针的指针
- 二级指针
指向指针的引用
int i =42;
int *p;
int* &r = p;
//r是一个对指针p的引用
面对一条复杂的指针或者引用的声明语句时,从右往左阅读
2.4const限定符
- const对象一旦创建后值不能再改变,所以const对象
必须初始化
.
初始化和const
- const对象的主要限制就是只能在const类型的对象上执行不改变其内容的操作.例如,const int和普通的int一样都能参与算术运算,也能转换成一个布尔值
- 初始化const对象时,可以利用一个对象对其进行初始化,则他们是不是const都无关紧要.
默认状态下,const对象仅在文件内有效
const int bufSize = 512;
- 编译器在编译过程中把用到该变量的地方都替换成对应的值,但是如果程序中包含多个文件,则每个用了const对象的文件都必须得能访问到他的初始值才行,要做到这一点就必须子啊每一个用到变量的文件中都要对他的定义,为此,避免对同一变量的重复定义默认情况下const对象被设定为仅在文件内有效.
- 如果想在多个文件中共享一个const对象,必须在变量的定义之前添加extern关键字,即对于const变量不管是声明还是定义都添加extern关键字
//f1.cpp定义并初始化一个常量,该常量能被其他文件访问
extern const int bufSize = fun();
//f2.h
extern const int bufSize; //与f1.cpp中的定义的是同一个变量
2.4.1const的引用
- 与普通的引用不同的是,对常量的引用不能被用作修改它所绑定的对象
初始化和对const的引用
//可以把引用绑定到const对象上,称为常量引用,与不同引用不同的是对常量的引用不能被用作修改它所绑定的对象
const int ci = 1
const int &r1 = ci //正确:引用机器对应的对象都是常量
r1=42; // error,r1是对常量的引用
int &r2 = ci; //错误,试图让一个非常量引用指向一个常量对象
const指针
- 由于指针是对象,允许把指针本身定义为常量,即
常量指针
- 常量指针必须初始化
const int *p; // 指向常量的指针,指向的对象的内容不能通过该指针改变,即*p不能改变
int * const p; //常量指针,指向不能改变,即p不能改变
课后题
int * const p;//error,没有初始化
const int *p;//正确,不必马上初始化
2.4.3顶层const
顶层const
表示指针本身是一个常量.底层const
表示所指的对象是一个常量
int i =0;
int *const p1 = &i; //不能改变pi的值,这是一个顶层const;
const int ci = 42; //不能改变ci的值,这是一个顶层const;
const int *p2 = &ci; //允许改变p2的值,这是一个底层const;
const int &r = ci; //用于声明引用的cosnt都是底层const
用于声明引用的cosnt都是底层const
对const的引用可能引用一个并非const的对象
2.4.2指针和const
2.4.4 constexpr和常量表达式
常量表达式
:是指值不会改变并且在编译过程就能得到计算结果的表达式- 显然,字面值是常量表达式,用常量表达式初始化的const对象也是常量表达式
# 1. 初始值为字面值和数据类型都满足要求
const int max_foles = 20
const int limit = max_files = 1
# 2.数据类型不满足要求
int staff_size = 27
# 3.初始值不满足要求
const int sz = get_size()
# get_size()的具体值直到运行时才能获取到而常量表达式的值需要再编译阶段就能得到计算结果.
- 一个对象是不是常量表达式由他的
数据类型
和初始值
决定
constexpr变量
- 将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式
constexpr int mf = 20
constexpr int limit = mf +1
const int sz = size()
// 只有当size()是一个constexpr函数时,才是一个正确的语句
一般来说,如果你认为变量是一个常量表达式,那就把他声明为一个constexpr类型
const函数
const
函数是指能用于常量表达式的函数- 函数的
返回值
类型及所有形参
的类型都得是字面值类型
,而且函数体中必须有且只有一条return语句
constexpr int new_sz() {return 42;}
constexpr int foo = new_sz(); //正确:foo是一个常量表达式
- 执行初始化任务时,编译器吧对constexpr函数的调用替换成其结果值,为了在编译过程中随时展开,constexpr函数被隐式的指定为内联函数.
- 我们允许constexpr函数的返回值并非一个常量
constexpr size_t scale(size_t cnt){return new_sz()*cnt;}
- 如果scale(arg)中arg是常量表达式,则scale(arg)也是常量表达式,即
scale(2)
是常量表达式,scale(i)不是常量表达式 int arr[scale(2)]
; 正确:scale(2)是常量表达式- int i = 2 ; int a2[scale(i)] ; 错误,scale(i)不是常量表达式.
constexpr 函数不一定返回常量表达式,当调用不正确时.scale(i)
字面值类型
- 定义:声明constexpr时用到的类型
- 算数类型,引用,指针都属于字面值类型
- 自定义类,IO库,string类型不属于字面值类型,也就不能被定义为constexpr
- 一个
constexpr指针
的初始值必须是nullptr
或者0
,或者存储于某个固定地址中的对象;一般定义在函数体类内的没有存储在固定地址中,定义在所有函数体之外的对象其地址固定.能用来初始化constexpr指针 - 允许函数定义一类有效范围超出函数本身的变量,这类变量有固定地址
指针和constexpr
- 在constexpr声明中如果定义一个指针,限定符constexpr
仅
对指针有效,与指针所指的对象无关.
const int *p = nullptr ;// p是一个指向整型常量的指针
constexpr int *p = nullptr; // q是一个指向整数的常量指针
- cosntexpr将他所定义的对象置为顶层const
constexpr int *np = nullptr; //np是一个指向整数的常量指针,其值为空
int j = 0;
constexpr int i = 40; //i的类型是整数常量
//假设i和j都定义在函数体之外
constexpr const int *p = &i; //p是常量指针,指向整型常量i
constexpr int *p1 = &j; //p1是常量指针,指向整数j
2.5处理类型
2.5.1类型别名
- 传统方法
typedef double wages;
typedef wages base,*p;
// wages是double的同义词,base是double的同义词,p是*double的同义词
- 使用别名声明
using SI = Sales_item;
将等号左侧的名字规定成等号右侧类型别名.
指针,常量和类型别名
typedef char *pstring;
const pstring cstr = 0; // cstr是指向char的常量指针
const pstring *ps; // ps是一个指针,它的对象是指向char的常量指针
错误理解
const pstring cstr =0 ;
//声明了一个指向char的常量指针
const char *cstr = 0;
//声明了一个指向cosnt char的指针
2.5.2 auto类型说明符
- auto让编译器通过初始值推算变量的类型
- auto定义的变量必须有初始值
- 使用auto也能在一条语句中声明多个变量,但是该语句中所有变量的初始基本数据类型都必须一样
auto i = 0,*p=&i; // 正确,i是整数,p整型指针
auto sz = 0,pi = 3.14;// 错误,类型不一样
符合类型,常量和auto
- auto一般会忽略掉底层const,同时底层const则会保留下来.
int i =0;
const int ci = i,&cr = ci;
auto b = ci; // b是一个整数(ci的顶层const特性被忽略掉)
auto c = cr; //c是一个整数
auto d = &i; //d是一个整型指针(整数的地址就是指向整数的指针)
auto e = &ci; //e是一个指向整数常量的指针(对常量对象去地址是一种底层const)
-但是如果希望推断出的auto类型是一个顶层const,需要明确指出
const auto f = ci;
- 还可以将引用的类型设为auto
auto &g = ci;
auto &h = 42; //error 不能为非常量引用绑定字面值
const auto &j = 42;
auto &n = i,*p2 = &ci;
//error i的类型是int而&ci的类型是const int
#include<iostream>
#include<typeinfo>
using namespace std;
int main()
{
const int i = 42;
auto j = i;
cout << "j be int," << typeid(j).name() << endl;
//j be int, int
return 0;
}
2.5.3 decltype类型指示符
- 有时候希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量.
- decltype的作用:选择并返回操作数的数据类型,编译器分析表达式并得到他的类型,却不实际计算表达式的值
decltype(f()) sum = x;
//sum的类型就是函数f的返回类型
//编译器并不实际调用函数f,而是使用当调用发生时f的返回值类型作为sum的类型
- decltype处理顶层const和引用的方式和auto有些许不同.如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内)
const int ci = 0,&cj = ci;
decltype(ci) x = 0; //x的类型const int
decltype(cj) y = x; //y的类型是const int &
decltype(cj) z; //error,z是一个引用必须初始化
decltype 和引用
- 如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型
int i=0,&r = i;
decltype(r) b = i; //b是int &类型
decltype(r+0) d; // d是未初始化的int
- 赋值是会产生引用的一类典型表达式,引用的类型就是左值的类型
int a = 3,b = 4;
decltype(a=b) d = a; // 的类型是decltype(a) &类型
编译器分析表达式并得到它的类型作为d的推断类型,但是并不实际计算该表达式.
- 如果表达式的内容是
解引用操作
,则decltype将得到引用
类型
int * p = &r;
decltype(*p) c; //error,c是int&类型,必须初始化
- decltype所用表达式,如果变量名加上了一对括号,则得到的类型与不加括号不一样.不加括号得到的类型就是变量的类型;但是如果加上括号,编译器就会把他当做一个表达式.
decltype((i)) d; //error,d是int&,必须初始化
decltype((variable))的结果永远是引用,而decltype(variable)结果只有当variable本身是一个引用时才是引用.
decltype和auto的区别
- auto类型说明符用编译器
计算变量的初始值
来推断其类型,而decltype虽然也让编译器分析表达式并得到它的类型,但是不实际计算表达式的值
. - auto一般会忽略掉顶层const,而把底层const保留下来.与之相反,decltype会保留变量的顶层const
const int ci = 0,&cj = ci;
decltype(ci) x = 0; //x的类型const int
decltype(cj) y = x; //y的类型是const int &
-----------------------
const int ci = i,&cr = ci;
auto b = ci; // b是一个整数(ci的顶层const特性被忽略掉)
- 看是否加括号
2.6自定义数据结构
2.6.1 定义Sales_data类型
struct Sales_data{
string bookNo;
unigned units_sold = 0;
double revenue = 0.0;
};
类体右侧的表示结束的花括号必须写一个分号,这是因为类体后面可以经跟变量名以示该类型对象的顶用,所以分号不可少.
2.6.2 使用Sales_data类
头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明
预处理器概述
- 预处理器是在编译之前执行的一段程序,当预处理器看到#include标记时就会用指定的头文件的内容代替#include
- 头文件保护符依赖于预处理变量,预处理变量有两种状态:已定义和未定义
- #define指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:#ifdef当且仅当变量已定义时为真,#ifndef当且仅当变量未定义时为真.一旦检查结果为真,则执行后续操作直至遇到#endif指令为止.----有效防止重复包含的发生
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include<iostream>
using namespace std;
struct Sales_data {
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
#endif