C++primer笔记——第二章【变量和基本类型】

【第二章】 变量和基本类型


2.1 基本内置类型
1)算术类型:包含了字符、整型数、布尔值、浮点数
2)空类型:不对应具体的值,仅用于一些特殊的场合


2.1.1 算术类型
1、算术类型分两类:整型和浮点型


2、1个字=32比特=4个字节


3、64位编译器:
      char :1个字节!最小尺寸为8位!
      char*(即指针变量): 8个字节!
      short int : 2个字节
      int:  4个字节!
      unsigned int : 4个字节
      float:  4个字节!32位!
      double:   8个字节!64位!
      long:   8个字节
      long long:  8个字节
      unsigned long:  8个字节


4、除去布尔型和扩展的字符型外,其他整型可分为带符号的和无符号的。带符号类型可以表示正数、0、负数,无符号类型则仅能表示大于等于0的值。


2.1.2 类型转换
1、当给带符号类型一个超出它表示范围的值时,结果是未定义的。如signed char c = 256;//假设char占8比特,c的值是未定义的。


2、含有无符号类型的表达式: 没学好


2.1.3 字面值常量
1、一个形如42的值被称作字面值常量,这样的值一望而知。每个字面值常量都对应一种数据类型,字面值常量的形式和值决定了它的数据类型。


2、可以将整型字面值写作十进制、八进制、十六进制数的形式。以0开头的整数代表八进制数,以0x或0X开头的代表十六进制数。


3、浮点型字面值表现为一个小数或以科学计数法表示的指数,其中指数部分用E或e标识。


字符和字符串字面值:
4、编译器在每个字符串的结尾处添加一个空字符('\0'),因此字符串字面值的长度要比它的内容多1。例如字符串“A”则代表了一个字符的数组,该数组包含两个字符。


转义序列:
5、有两类字符不能直接使用:
1)不可打印的字符,如退格(\b)或其他控制字符,因为他们没有可视的图符
2)在C++中有特殊含义的字符(单引号,双引号,问号,反斜线)。
在这些情况下就需要用到转义序列,均以反斜线作为开始。


2.2 变量
初始值:
1、当对象在创建时获得了一个特定的值,则这个对象就被初始化了。


2、初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新的值来替代。


列表初始化:
int i=0;
int i={0};
int i{0};
int i(0);
3、当用于内置类型的变量时,花括号初始化形式有一个重要的特点:如果使用列表初始化且初始值存在丢失信息的风险,则编译器将报错。
如:
long double i=3.1415926536;
int a{i}, b={i};   // 错误,转换未执行,因为存在丢失信息的危险
int c(i), d=i;     // 正确,转换执行,但丢失了部分值


默认初始化:
4、如果定义变量时没有指定初值,则变量被默认初始化,默认值到底是什么由变量类型决定,同时定义变量的位置也会对此有影响。


5、如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。定义于任何函数体之外的变量被初始化为0。


6、对string类型的对象,因为string类本身可以接受无参数的初始化方法,故无论变量定义在函数体内、外都被默认初始化空串。


2.2.2 变量声明和定义的关系
1、分离式编译机制允许将程序分割为若干个文件,每个文件可被独立编译。为支持分离式编译,C++将声明和定义区分开来。声明使得名字为程序所知,定义负责创建与名字关联的实体


2、变量声明规定了变量的类型和名字,在这点上定义与之相同。但除此之外,定义还申请存储空间,也可能会为变量赋初值。


3、如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而不要显式地初始化变量。如extern int i;


4、变量能且只能被定义一次,但是可以被多次声明。


5、如果要在多个文件中使用同一个变量,就必须将声明和定义分离。此时,变量的定义必须出现且只能出现在一个文件中,而用到该变量的其他文件必须对其进行声明,却绝对不能重复定义!


2.2.3 标识符
C++的标识符由字母、数字、下划线组成,其中必须以字母或下划线开头。没有长度限制,大小写敏感。


变量命名规范:
1)变量名一般用小写。
2)用户自定义的类名一般以大写字母开头
3)如果标识符由多个单词组成,单词间应有明显区分。如 student_loan 或 studentLoan,不要使用studentloan

2.2.4 名字的作用域
1、同一个名字在不同的作用域中可能指向不同的实体。名字的有效区域始于名字的声明语句,以声明语句所在的作用域末端为结束

2、全局作用域内的名字在整个程序范围内都可以使用。

嵌套的作用域:
3、作用域能彼此包含,被包含的作用域称为内层作用域。包含着别的作用域的称为外层作用域。

4、显式地访问全局变量用作用域运算符::

2.3 复合类型
复合类型是指基于其他类型定义的类型(如引用和指针)

2.3.1 引用
1、引用为对象起了另外一个名字。引用类型 引用另外一种类型。而引用必须初始化。如 int &j = i;// j指向i,是i的另外一个名字

2、定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,所以引用必须初始化。

引用即别名:
3、引用并非对象,相反地,它只是为一个已经存在的对象所起的另一个名字。所以不能定义引用的引用。

4、引用只能绑定在对象上,而不能与字面值或者某个表达式的计算结果绑定在一起!!!

2.3.3 指针
指针是指向另外一种类型的复合类型。

指针与引用的不同点:
1)指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。
2)引用不是对象,无法令引用重新绑定到另一个对象上。
3)指针无需再定义时赋初值。

获取对象的地址:
1、指针存放某个对象的地址,要想获取该地址,需要使用取地址符&

2、因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。

3、在声明语句中指针的类型实际上被用于指定它所指向对象的类型,所以二者必须匹配。

指针值:
指针的值应属于下列4种状态之一:
1)指向一个对象
2)指向紧邻对象所占空间的下一位置
3)空指针,意味着指针没有指向任何对象
4)无效指针,也就是上述情况之外的其他值

利用指针访问对象:
4、如果指针指向了一个对象,则允许使用解引用符*来访问该对象。解引用操作仅适用于那些确实指向了某个对象的有效指针。

空指针:
5、空指针不指向任何对象。生成空指针的方法:
1)int *p1 = nullptr;
2) int *p2 = 0;   // 直接将p2初始化为字面常量0
// 需要先include cstdlib
3) int *p3 = NULL; // 使用NULL预处理变量给指针赋值

6、把int变量直接赋值给指针是错误的操作,即使该变量是0也不可以。

void* 指针:
7、这是一种特殊的指针类型,可用于存放任意对象的地址。

8、不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。

指向指针的引用:
因为指针是对象,所以存在对指针的引用。如:
int *p;    // p是一个int型指针
int *&r = p;  // r是一个对指针p的引用
int i = 42;
r = &i;   // r引用了一个指针,因此给r赋值&i就是令p指向i
*r = 0;  // 解引用r得到i,也就是p指向的对象,将i的值改为0
要理解r的类型到底是什么,最简单的办法就是从右向左阅读r的定义。离变量名最近的符号(本例是&)对变量类型有最直接的影响,因此r是一个引用。
声明符的其余部分用以确定r引用的类型是什么,本例中的*说明r引用的是一个指针。最后,声明的基本数据类型部分指出r引用的是一个int指针。

2.4 const限定符
作用:
1)容易对变量进行调整   2)警惕防止程序一不小心改变了这个值

1、const对象一旦创建后其值就不能再改变,因此const对象必须初始化。


初始化和const:
2、const主要的限制就是只能在const类型的对象上执行不改变其内容的操作。

3、在不改变const对象的操作中还有一种是初始化,如果利用一个对象去初始化另外一个对象,则他们是不是const都无关紧要。如:
int i = 43;
const int ci = i;  // 正确
int j = ci;        // 正确
ci的常量特征仅在执行改变ci的操作时才会发生作用。

默认状态下,const对象仅在文件内有效
4、
当定义const int size = 512;之后,编译器在编译过程中把用到该变量的地方都替换成对应的值。
为执行上述的替换,编译器必须知道变量的初始值。如果程序包含多个文件,则每个用了const对象的文件都必须能访问到它的初始值才行。但这必须要在每个用到变量的文件中都有对它的定义。
为了支持这一用法,同时避免对同一变量的重复定义,默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,等同于在不同文件中分别定义了独立的变量。
但我们想让这类const对象像其他(非常量)对象一样工作,即只在一个文件中定义const,而在其他多个文件中声明并使用它。
解决的办法:对于const变量不管是声明还是定义都添加extern关键字,这样只需定义一次就可以了。如:
// file_1.cc 定义并初始化了一个常量,该常量能被其他文件访问
extern const int i = fcn();
// file_1.h 头文件
extern const int i; // 与 file_1.cc中定义的i是同一个
如上述程序所示, file_1.cc定义并初始化了i,因为这条语句已经包含了初始值,所以它显然是一次定义。
然而,因为i是一个常量,必须用extern加以限定使其被其他文件使用。
file_1.h头文件中的声明也由extern做了限定,作用是指明i并非本文件所独有,它的定义在别处出现。
 
总结:如果想在多个文件之间共享const对象,必须在变量的定义之前加extern关键字。
2.4.1 const的引用
5、可以把引用绑定到const对象上,就像绑定到其他对象上一样,称之为对常量的引用。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象。如:
const int ci = 1024;
const int &r1 = ci;           // 正确,引用及其对应的对象都是常量
r1 = 42;                      // 错误,r1是对常量的引用
int &r2 = ci;                 // 错误,试图让一个非常量引用指向一个常量对象,否则你就能通过r2去修改常量ci了

初始化和对const的引用
6、引用的类型必须与其所引用对象的类型一致,但是有两个例外:
1)在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。
2)允许为一个常量引用绑定非常量的对象、字面值,甚至是一个表达式
int i = 42;                       
const int &r1 = i; // 允许将const int&绑定到一个普通int对象上
const int &r2 = 42; // 正确,r1是一个常量引用
const int &r3 = r1*2; // 正确,r1是一个常量引用
int &r4 = r1*2; // 错误,r4是一个普通的非常量引用

对const的引用可能引用一个并非const的对象:
7、一个常量引用如const int &r = i,r指向一个非常量i时,是合法的,只是不允许通过r去修改i,但可以用别的方法修改i的值

2.4.2 指针和const
8、与引用一样,也可以令指针指向常量或非常量。指向常量的指针不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针。如:
const double pi = 3.14;
double *ptr = π // 错误,ptr是一个普通指针
const double *cptr = π // 正确,cptr可以指向一个双精度常量
*cptr = 42; // 错误,不能给*cptr赋值

9、指针的类型必须与其所指对象的类型一致,但是有两个例外:
1)允许令一个指向常量的指针指向一个非常量对象,但是不能通过该指针改变所指向的非常量对象的值。
和常量引用一样,指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。

const指针:
10、指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定义为常量。

11、常量指针必须初始化!!而且一旦初始化完成,它的值就不能再改变。把*放在const前用以说明指针是一个常量,说明不变的是指针本身而不是指针所指向的那个值
如:int i=0;     int *const p = &i;
指针是一个常量并不意味着不能通过用指针修改其所指对象的值。能否这样做完全依赖于所指对象的类型。

12、
int errNumb = 0;
int *const curErr = &errNumb;   // curErr将一直指向errNumb
const double pi = 3.14159;
const double *const pip = π // pip是一个指向常量对象的常量指针
不论是pip所指向的对象值还是pip自己存储的那个地址都不能改变。相反地,curErr指向的是一个一般的非常量整数,完全可以用curErr去修改errNumb的值。

一旦指针指向的是常量,就不能用指针去改变所指对象的值。

常量不能被赋值,不管是变量还是指针。

2.4.3 顶层const
13、用名词 顶层const表示指针本身是一个常量,而用名词 底层const表示指针所指的对象是一个常量。

14、一般顶层const可以表示任意的对象是常量,这一点对任何数据类型都适用。底层const则与指针和引用等复合类型的基本类型部分有关。

15、特殊的是,指针类型既可以是顶层const也可以是底层const。

16、当执行对象的拷贝操作时,常量是顶层const还是底层const区别明显。其中,顶层const不受什么影响。

17、另一方面,底层const的限制不能忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。

2.4.4 constexpr 和常量表达式
18、常量表达式是指不会改变并且在编译过程就能得到计算结果的表达式。如const int sz = get();中,尽管sz本身是一个常量,但它的具体值直到运行时才能获取,故不是常量表达式

constexpr变量:
19、允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化:
constexpr int mf = 20; // 20是一个常量表达式
constexpr int limit = mf + 1; // mf+1是常量表达式
constexpr int sz = size();  // 只有当size是一个constexpr函数时才是一个正确的声明语句

字面值类型:
一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象。

指针和constexpr:
20、在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关:
const int *p = nullptr;   // p是一个指向整型常量的指针
constexpr int *q = nullptr; // q是一个指向整数的常量指针,是一个常量指针
其中的关键在于,constexpr把它所定义的对象置为了顶层const。

2.5 处理类型


2.5.1 类型别名
1、有两种方法可以用于定义类型别名。
1)传统方法,使用typedef
typedef double wages; // wages是double的同义词
typedef wages base, *p; // base是double的同义词,p是double*的同义词
2)使用别名声明来定义类型的别名
using SI = Sales_Ittem;     // 把等号左侧的的名字规定成等号右侧类型的别名

指针、常量和类型别名:
typedef char *pstring;
const pstring cstr = 0;
这里cstr是指向char的常量指针,千万不能把char *代入进去,应把pstring当做一个类型名来处理。

2.5.2 auto类型说明符
用它让编译器替我们去分析表达式所属的类型。auto定义的变量必须有初始值。

21、使用auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型必须一样。
auto sz = 0, pi = 3.14;  // 错误,两个变量的类型不一致!!!

复合类型、常量和auto:
22、auto一般会忽略掉顶层const,同时底层const则会保留下来。比如当初始值是一个指向常量的指针时:
const int ci = i, &cr = ci;
auto b = ci; // b是一个整数(ci的顶层const特性被忽略了)
auto c = cr; // c是一个整数(cr是ci的别名,ci本身是一个顶层const)
auto d = &i; // d是一个整型指针(整数的地址就是指向整数的指针)
auto e = &ci; // e是一个指向整型常量的指针(对常量对象取地址是一种底层const)

如果希望推断出的auto类型是一个顶层const,需要明确指出!!
const auto f = ci; // ci的推演类型是int,f是const int

2.5.3 decltype类型指示符
希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。则用到decltype,作用是选择并返回操作数的数据类型。
在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值!!
decltype(f()) sum = x;  // sum的类型就是函数f()的返回类型
编译器不实际调用函数f,编译器为sum指定的类型是假如f被调用时将会返回的那个类型。

23、decltype处理顶层const和引用的方式与auto有些不同。如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内):
const int i = 42;
auto j = i; // j是一个整数
const auto &k = i; // k是一个整型常量
auto *p = &i; // p是一个指向整型常量的指针

decltype(i) x = 0; // x的类型是const int

const int &cj = i;
decltype(cj) y = x; // y的类型是const int& ,y绑定到变量x
decltype(cj) z; // 错误,z是一个引用,必须初始化

decltype和引用:
24、如果decltype使用的表达式不是一个变量,则decltype返回表达式的结果对应的类型。
// decltype的结果可以是引用类型
int i = 42, *p = &i, &r = i;
decltype(r + 0) b; // 正确,加法的结果是int,因此b是一个(未初始化)的int
decltype(*p) c; // 错误,c是int&,必须初始化
因为r是一个引用,因此decltype(r)的结果是引用类型,如果想让结果类型是r所指的类型,可以把r作为表达式的一部分,如r+0,显然这个表达式的结果是一个具体的值
另一方面,如果表达式的内容是解引用操作,则decltype将得到引用类型。因此decltype(*p)的结果就是int&,而非int

25、对于decltype所用的表达式来说,如果变量名加上了一对括号,则得到的类型与不加括号时会有不同。
1)如果使用不加括号的变量,则得到的结果就是该变量的类型,如decltype(i) e; // e是一个未初始化的int
2)如果给变量加上了一层或多层括号,编译器就会把它当做一个表达式。于是得到的结果就会是引用类型,如decltype((i)) f; // f是int&,必须初始化
切记!!decltype((variable))的结果永远是引用,而decltype(variable)的结果只有当variable本身就是一个引用时才是引用。

26、赋值是会产生引用的一类典型表达式,引用的类型就是左值的类型。即:如果i是int,则表达式i=x的类型是int&。
int a = 3, b = 4;
decltype(a = b) d = a; // 表达式a=b作为参数,并不实际计算该表达式,于是a的值不变,且d的类型是int&,d是a的别名。

27、auto和decltype的区别:
1)auto类型说明符用编译器计算变量的初始值来推断其类型,而decltype虽然也让编译器分析表达式并得到它的类型,但是不实际计算表达式的值
2)auto一般会忽略掉顶层const,而保留底层const。相反地,decltype会保留变量的顶层const
3)decltype的结果类型与表达式的形式密切相关,如果变量名加上了一对括号,则得到的类型与不加括号时会有所不同。见25(2)

const int d=5; // d是一个常量,含有顶层const
auto f1 = d; // auto自动推断类型自动忽略顶层const,所以f1推断结果是整数
decltype(d) f2 = d; // decltype保留顶层const,所以f2推断结果是整型常量。

f1++; // 正确
f2++; // 错误,f2是常量,值不能改变。

2.6.3 编写自己的头文件
28、头文件通常包含那些只能被定义一次的实体,如类、const和constexpr变量等。

预处理器概述:
29、确保头文件多次包含仍能安全工作的技术是预处理器。











猜你喜欢

转载自blog.csdn.net/CSDN_dzh/article/details/80908154