文章目录
第二章:变量和基本类型
C++支持广泛的数据类型,既定义了几种基本内置类型(知乎,整数,浮点数等),也定义了一些更复杂的数据类型,例如可变字符串和向量。本章讨论内置类型。
一.基本内置类型
基本类型分为算术类型和空类型。空类型(void)不对应具体的值,仅用于一些特殊场合。
算术类型又分为整型和浮点型。整型除了布尔型和扩展字符型外,分为有符号和无符号。字符型有三种,char, signed char, unsigned char,char到底被实现为后面两种的哪一种取决于编译器(所以说不能认为char 和 unsigned char 相同)。在算术表达式中不要使用char,因为它有无符号不确定。执行浮点数运算用double,因为double并不比float慢。
当把整数赋给浮点类型的时候,精度可能会有损失。
当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数,例如unsigned char是%256
当我们赋给有符号类型一个超出它表示范围的值时,结果是未定义的。
当一个算术表达式中既有无符号数又有int值时,那个int值会转换为无符号数。即如果表达式中既有无符号数又有带符号数,那么带符号数会自动转换成无符号数。所以切勿混用带符号类型和无符号类型。
对于整型字面值,0开头代表八进制,0x或0X开头表示十六进制。默认情况下,十进制数是带符号的,而八进制和十六进制不确定。十进制是int long longlong中能容纳值最小的那个,八进制或十六进制是int unsigned int long un…中能容纳值最小的那个。对十进制字面值来说,其实不会是负数,因为-42对它来说代表对42取负值,负号不在字面值中。
浮点数可以用E或e来 表示指数,例如3.14159E0, .001等默认浮点型字面值是double。
字符串以’\0’表示结束,可以换行写,即写在两个字符串中,见书P36例子。
泛化的转义序列,\后可跟8进制数或十六进制数来对照ASCII码表从而打印出相应信息。
可以通过指定字面值的类型改变字面值的类型,如U,F,LL等。见书P37表2.2。
nullptr是指针字面值。
二.变量
变量代表这一块内存空间,该空间的大小和布局方式,以及变量能参与的运算。
变量需要初始化,我们要知道在C++语言中初始化和赋值是两个完全不同的操作,即我们有时用=来初始化,但严格来说这不是赋值,虽然大多数时候两者的区别无关紧要。初始化的含义是创建变量的时候赋予其一个初始值,赋值的含义是把对象的当前值擦去,以一个新值来代替。
C++11全面引入了列表初始化,即用花括号来初始化或赋值,其有一个特点:使用列表初始化且初始值存在丢失的风险,则编译器将报错(例如默写隐式转换)。
默认初始化;如果变量没有被显式初始化,则会被默认初始化,定义于任何函数之外的变量都被默认初始化为0,定义在函数内部的内置变量都不被默认初始化。
C++支持分离式编译,即把程序分割为若干个文件,每个文件可被独立编译。为了支持分离式编译,C++将声明与定义分开,可以用extern关键字来声明变量,声明知识规定变量的类型和名字,但没有申请存储空间,任何显式的初始化都是定义,即使加了extern,在函数内部加了extern还显式初始化会报错。
C++为标准库保留了一些名字,所以用户自定义的标识符不要连续出现两个下划线,也不要以下划线紧连大写字母开头,在函数体外的标识符也不要以下划线开头。
C++中的作用域大多数以花括号分割。
当你第一次使用变量的时候再定义它,方便清楚变量含义以及赋初始值。
同一作用域中变量不能重复定义。
三.复合类型
复合类型是基于其他类型定义的类型,如引用和指针。
C++中一条声明语句由一个基本数据类型和后面的声明符列表组成,声明符命名了一个变量与指定其为与前面的基本数据类型有某种关系的类型。
引用必须被初始化,引用无法重新绑定到另一个对象,可以传递绑定。
所有引用的类型都要与与之绑定的类型严格匹配,且引用必须绑定在对象上,不能是字面值常量。
如果要在一个语句中定义多个指针,则每个变量前都要有*
引用没有地址,不能定义指向引用的指针。
所有指针的类型都要和它指向的类型严格匹配。(除了两种特殊情况)
空指针:空指针可以用nullptr, 0, NULL来表示,最好使用nullptr,尽量避免使用NULL,不能用变量的值直接初始化指针,即使变量的值有效。指针最好一开始就初始化,即使初始化为空。
void*指针:void*指针可用于存放任意对象的地址,但是我们对该地址中是什么类型的对象并不清楚,不能直接操作void*指针指向的对象。
最好把类型修饰符与变量写在一起。
阅读复杂的声明语句,对声明符可以从右往左读,如 int *&r,其首先是一个引用,其次引用的对象是指针,再其次该指针指向int型数据,所以r是一个指向int型数据的指针的引用。
四.const限定符
const对象必须初始化。
const的常量特征只有在执行改变其值的时候才会发挥作用。
默认状态下,const对象仅在文件内有效,这句话是什么意思呢:当我们直接在定义时用常量表达式初始化一个const时,编译器会把程序中用到const的地方换成相应的常量表达式,即每个文件中使用该const常量的值相同,但这并不代表这每个文件中的const常量是同一个const,而是他们都有自己的 const常量,只是值相同而已,要达到这一效果,可把const常量定义在头文件中,每个包含了该头文件的源文件即可拥有相同值的const常量。但是,如果const常量不是由常量表达式定义的,那么在头文件中定义const常量并不能保证各个源文件const常量值相同,这时需要在const常量中用extern声明该const常量,在源文件定义const常量时也需要加上extern关键字。
即:在头文件定义的const常量,包含该头文件的源文件都有对应的一个const常量,在源文件定义的const常量要被其他源文件使用,则在定义的时候要加上extern关键字,且要在头文件中用extern加上该常量的声明,这样包含了该头文件的源文件可以使用该常量。
一些const对象定义在头文件中。而const变量要成为常量表达式,初始化式必须为编译器可见。为了能够让多个文件使用相同的值,const变量和它的初始化式必须是每个文件可见的。而要使初始化式可见,一般把这样的const变量定义在头文件中。那样的话,无论该const变量何时使用,编译器都能够看见其初始化式。
但是,C++中的任何变量都只能定义一次,定义会分配存储空间,而所有对该变量的使用都关联到同一存储空间。因为const对象默认为定义它的文件的局部变量,所以把它们的定义放在头文件中是合法的。
当我们在头文件中定义了const变量后,每个包含该头文件的源文件都有了自己的const变量,其名称和值都一样。
当该const变量是用常量表达式初始化时,可以保证所有的变量都有相同的值。但是在实践中,大部分的编译器在编译时都会用相应的常量表达式来替换对这些const变量的使用。所以,在实践中不会有任何存储空间用于存储用常量表达式初始化的const变量。
如果const变量不是用常量表达式初始化,那么它就不应该在头文件中定义。相反,和其他的变量一样,该const变量应该在一个源文件中定义并初始化。应在头文件中为它添加extern声明,以使其能被多个文件共享。
From : http://blog.csdn.net/snlying/article/details/4524397
这篇文章关于这个问题说的很好: c++中const变量定义与头文件包含的有关问题
const的引用类型必须匹配。即const int的引用为const int &
常量引用引用的对象可以是任意类型,只是有可能生成中间临时量(这是引用类型匹配的一种例外)。即,对const的引用可能引用一个并非const的对象。
关于常量指针和引用差不多,常量的指针必须是对应的类型,但是指向某种常量类型的指针可以指向任意类型。
常量指针:所谓常量指针,就是该指针本身是一个常量,如const int *const pi = …,即*const代表常量指针。从右往左读,pi是一个常量,pi是一个指针,pi指向(自以为)const int型数据。当然,常量指针也要定义即初始化。
用名词顶层const表示指针本身是一个常量,用底层const表示指针指向的对象是一个常量。更一般的,顶层const可以表示任意的对象是常量,底层const则与指针和引用等复合类型的基本类型部分有关。
当执行对象的拷贝操作时,顶层const并不受什么影响,但是底层const的影响很大,即拷入和拷出的对象必须具有相同的底层const,或者两个对象的数据类型能够转换。这个用于判断const的拷贝,不过很多时候凭之前的知识就可以判断能够拷贝或者转换。
常量表达式:指值不会改变并且在编译过程中就能得到计算结果的表达式,一个对象或表达式是不是常量表达式由它的数据类型和初始值共同决定。
可以使用constexpr来声明或定义某个对象是常量表达式。可以定义一种特殊的constexpr函数来初始化常量表达式。
constexpr所用到的类型必须是字面值类型,即算术类型,指针,引用。且指针的初始值必须是nullptr或存储于某个固定地址的对象,反正是编译过程中就能得到的值。
明确一点,constexpr修饰的就是后面的对象,而不是对象指向的对象,例如constexpr int *p代表p是一个常量指针,即constexpr总是把它所定义的对象置为顶层const。而constexpr const int* p则代表指向const int的常量指针。
五.处理类型
类型别名:类型别名即给某种类型起一个同义词。有两种方法,typedef和using。
typedef:同之前的定义变量类似,typedef 后面的声明符可以有类型修饰。例如
typedef double wages;
typdef wages base, *p; // p即为double*类型
using:using的用法即为using后跟别名等号和类型
using wages = double;
using p = double*;
如果某个类型别名指代的是复合类型或常量,那么可能会产生意想不到的后果。特别是我们不能简单的用替换的方法判断某个用类型别名定义的对象类型。如下
typedef char *pstring;
const pstring cstr = 0;
const pstring *ps;
如果用替换的方法就会理解错,因为我们这里直接把pstring当作一种类型,所以const直接修饰后面的声明符了。即cstr是一个指向char的常量指针,ps是一指针,它的对象是指向char的常量指针。所以复合类型的类型别名与const结合起来很容易出错。
auto:auto让编译器通过初始值来推断变量的类型,显然auto定义的变量必须有初始值。使用auto也能在一条语句中定义多个变量,但是多个变量的初始基本数据类型必须一样。
当在使用某个引用时,要看作直接在使用原来的对象,特别是判断顶层和底层const时。
auto一般会忽略顶层const,保留底层const,不过设置引用的时候会保留顶层const。
decltype的作用是选择并返回操作数的操作类型,在此过程中,编译器分析表达式并得到它的类型,但是不实际计算表达式的值。
decltype保留顶层的const以及引用,例如
const int ci = 0, &cj = ci;
decltype(cj) z; // 错误,const int&要初始化
特别要注意的是,前面说过引用都是作为其所指对象的同义词出现,只有在decltype是一个例外。
decltype中的表达式的内容如果是解引用操作,则decltype将得到引用类型
decltype的结果类型与表达式形式密切相关,如果给变量加上括号得到的则为变量的引用。即decltype((var))得到的永远是变量的引用,而decltype(var)除非变量本身是一个引用,否则不会是引用。
六.自定义数据结构
C++11新标准规定,可以为类(或结构体)内的数据成员提供一个类内初始值。没有初始值的成员将被默认初始化。
为了确保各个文件中的类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样。
确保头文件多次包含仍能安全工作的常用技术是预处理器,#include是一项预处理器功能,当预处理器看到#include标志时会用指定的头文件的内容代替#include。
还有一项预处理器的功能是头文件保护符,头文件保护符依赖于预处理变量,预处理变量有两种状态:已定义和未定义。#define 指令把一个名字设定为预处理变量,#ifdef当变量已定义为真,#ifndef相反,两者为真后一直执行后续到#endif。
整个程序中的预处理变量包括头文件保护符必须唯一,一般用头文件中的类的名字来构建保护符的名字。
预处理变量无视C++中关于作用域的规则。
练习
答案参考:第 2 章
2.1
int、long、long long 和 short 表示整型类型,C语言规定它们表示数的范围 short \leq≤ int \leq≤ long \leq≤ long long。其中,数据类型 long long 是在 C11 中新定义的;
除去布尔型和扩展的字符型之外,其它整型可以划分为**带符号的(signed)和无符号的(unsigned)**两种。带符号类型可以表示正数、负数和 0,无符号类型则仅能表示大于等于 0 的值。类型 int、short、long 和 long long 都是带符号的,通过在这些类型名前添加 unsigned 就可以得到无符号类型,例如 unsigned long。类型 unsigned int 可以缩写为 unsigned;
float 和 double 用于表示浮点数,其中,float 表示单精度浮点数,double 表示双精度浮点数。执行浮点数运算选用 double,这是因为 float 通常精度不够而且双精度浮点数和单精度浮点数的计算代价相差无几。事实上,对于某些机器来说,双精度运算甚至比单精度还快。long double 提供的精度在一般情况下是没有必要的,况且它带来的运算时消耗也不容忽视。
2.2
利率(rat):double,本金(principal):long long,付款(payment):long long。
利率一般是小数点后保留四位有效数字;本金和付款使用最大的带符号整型表示。
2.3
#include <iostream>
#include "sales_item.h"
int main() {
std::cout << sizeof(unsigned) << std::endl;
std::cout << sizeof(int) << std::endl;
std::cout << "**************************" << std::endl;
unsigned u = 10, u2 = 42;
std::cout << u2 - u << std::endl;
std::cout << u - u2 << std::endl;
std::cout << "**************************" << std::endl;
int i = 10, i2 = 42;
std::cout << i2 - i << std::endl;
std::cout << i - i2 << std::endl;
std::cout << i - u << std::endl;
std::cout << u - i << std::endl;
return 0;
}
2.5
(a) ‘a’, L’a’, “a”, L"a"
字面值 数据类型
‘a’ 字符字面值,类型是 char
L’a’ 宽字符字面值,类型是 wchar_t
“a” 字符串字面值
L"a" 宽字符串字面值
(b) 10, 10u, 10L, 10uL, 012, 0xC
字面值 数据类型
10 整型字面值,类型是 int
10u 无符号整型字面值,类型是 unsigned int
10L 整型字面值,类型是 long
10uL 无符号整型字面值,类型是 unsigned long
012 八进制整型字面值,类型是 int
0xC 十六进制整型字面值,类型是 int
(c) 3.14, 3.14f, 3.14L
字面值 数据类型
3.14 浮点数字面值,类型是 double
3.14f 浮点数字面值,类型是 float
3.14L 浮点数字面值,类型是 long double
(d) 10, 10u, 10., 10e-2
字面值 数据类型
10 整型字面值,类型是 int
10u 无符号整型字面值,类型是 unsigned int
10. 浮点数字面值,类型是 double
10e-2 浮点数字面值,类型是 double
2.6
有区别,两组定义的都是int型的整型字面值,但是一组是八进制,另一组是十进制,所以第二组的09是不合法的。
2.7
字面值 数据类型
“Who goes with F\145rgus?\012” 字符串字面值,包含两个八进制转义序列
3.14e1L 浮点数字面值,类型是 long double
1024f 浮点数字面值,类型是 float
3.14L 浮点数字面值,类型是 long double
2.8
#include <iostream>
int main() {
std::cout << "\62\115\n";
std::cout << "\62"
<< "\t"
<< '\115'
<< '\n';
return 0;
}
2.9
(a) 定义非法,数据不能在cin >> 后定义
(b) 定义非法,使用列表初始化时初始值有丢失编译器会报错
(c) 定义非法, wage未定义
(d) 可以这样定义,但是编译时如果开启了-Wall选项会警告
2.10
按C++规定应是
global_str : 空
global_str : 0
local_int : 未定义
local_str : 未定义,取决于string类默认的初始值
程序运行输出为
global_str =
global_int = 0
local_int = -406316616
local_str =
2.11
(a) 定义,如果其他文件还定义了x,则会报错重复定义
(b) 定义
(c) 声明
2.12
(a) 不合法 (b) 合法 (c) 不合法 (d) 不合法 (e) 合法
2.13
100
2.14
合法,输出100 45
2.15
(a) 合法 (b) 不合法 (c) 合法 (d ) 不合法
2.16
(a) 合法
(b) 合法,自动类型转换
(c) 合法
(d) 合法
2.17
10 10
2.18
#include <iostream>
int main() {
int ival = 0, *pi = &ival;
std::cout << "初始值:" << ival << ' ' << pi << std::endl;
*pi = 10;
pi = nullptr;
std::cout << "修改值:" << ival << ' ' << pi << std::endl;
return 0;
}
初始值:0 0x7fff483a9a6c
修改值:10 0
2.19
引用
引用(reference)为对象起了另外一个名字,引用类型引用(refers to)另外一种类型。
int ival = 1024;
int &refVal = ival; // refVal 指向 ival(是 ival 的另一个名字)
int &refVal2; // 报错:引用必须被初始化
int &refVal4 = 10; // 错误:引用类型的初始值必须是一个对象
// 除特殊情况,其他所有引用的类型都要和与之绑定的对象严格匹配
double dval = 3.14;
int &refVal5 = dval; // 错误:此处引用类型的初始值必须是 int 型对象
指针
指针(pointer)是"指向(point to)"另外一种类型的复合类型。与引用类似,指针也实现了对其他对象的间接访问。然而指针与引用相比又有很多不同点。
指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象;
指针无须在定义时赋初值;
因为引用不是对象,没有实际地址,所以不能定义指向引用的指针;
除特殊情况,其他所有指针的类型都要和它所指向的对象严格匹配;
空指针的定义方法:
int *p1 = nullptr; // C++11 新标准引人的方法,推荐使用
int *p2 = 0; // 直接将 p2 初始化为字面值常量 0
// 需要首先 #include cstdlib
int *p3 = NULL;
三种定义空指针的方法等价,推荐使用第一种。
虽然指针无须在定义时赋初值,但是记得初始化所有指针。如果实在不清楚指针应该指向何处,就把它初始化为 nullptr 或者 0,这样程序就能检测并知道它没有指向任何具体的对象了。使用未经初始化的指针是引发运行时错误的一大原因。
2.20
使 i 的值变为原来 i 的值的平方。
2.21
(a) 非法,类型不匹配 (b) 非法,不能用变量值直接初始化指针 (c) 合法
2.22
if (p) :如果p为空指针则为false,否则为true
if (*p):如果p指向的值为0则为false,否则为true
2.23
No, you can’t. Because it would be expensive to maintain meta data about what constitutes a valid pointer and what doesn’t, and in C++ you don’t pay for what you don’t want.
However, a smart pointer can be used to tell if it points to a valid object.
简而言之,不能,但是智能指针可以判断是否有效。
2.24
类型不匹配
2.25
(a) ip : 指向int型对象的指针,值不确定
i : int型变量,值不确定
r : int型变量的引用
(b) i : int,值不确定
ip : int*,0
(c) ip : int*,值不确定
ip2 : int,值不确定
2.26
(a) 不合法,const int 定义即要初始化
(b) 合法
(c) 合法
(d) 不合法,const int 值不能改变
2.27
(a) 不合法 int& 不能引用const int
(b) 合法 int *const可以指向int
(c) 合法
(d) 合法
(e) 合法
(f) 不合法,引用本身不是对象,不能设为常量。(而且还没有初始化)
(g) 合法
2.28
(a) 不合法,int *const 需要初始化
(b) 不合法,int *const需要初始化
(c) 不合法,const int 需要初始化
(d) 没初始化
(e) 合法
2.29
(a) 合法
(b) 不合法,int* 不能指向 const int
(c) 不合法,int* 不能指向 const int
(d) 不合法,const int *const的值不能改变
(e) 不合法, int *const 的值不能被改变
(f) const int的值不能改变
2.30
const int v2 = 0 // 顶层const
int v1 = v2 // 无影响
int *p1 = &v1, &r1 = v1 // 无影响
const int *p2 = &v2 // 底层const
const int *const p3 = &i // 底层,顶层
const int &r2 = v2 // 底层
2.31
r1 = v2 // 合法,r1绑定的v1,const int 可以赋给int
p1 = p2 // 不合法,const int* 不可以赋给 int*(或者说底层const匹配也行)
p2 = p1 // 合法,int* 可以赋给const int*
p1 = p3 // 不合法,底层const不匹配
p2 = p3 // 合法,底层const匹配
// 主要是看底层const是否匹配,否则很容易绕晕
int main() {
int i;
const int v2 = 0; // v2 has top-level const
int v1 = v2;
int *p1 = &v1, &r1 = v1;
const int *p2 = &v2, *const p3 = &i, &r2 = v2; // p2 has low-level const
// p3 has both low-level and top-level const
// r2 has low-level const
r1 = v2; // OK
// p1 = p2; // Error: low-level const doesn't match
p2 = p1; // OK
//p1 = p3; // Error: low-level const doesn't match
p2 = p3; // OK
return 0;
}
2.32
int null = 0; // 合法
int *p = null; // 不合法,int型变量的值不能用来初始化int*变量,int *p = nullptr或int * p = &null
2.33
a:int b:int c:int d:int* e:const int* g:const int&
a = 42; // 合法
b = 42; // 合法
c = 42; // 合法
d = 42; // 不合法,int类型的值不能赋给int*
e = 42; // 不合法,int类型的值不能赋给const int*
g = 42; // 不合法,g是const int的常量,const int的值不能改变
2.34
#include <iostream>
using namespace std;
int main(int argc, char *argv[]) {
int i = 0, &r = i;
auto a = r;
const int ci = i, &cr = ci;
auto b = ci;
auto c = cr;
auto d = &i;
auto e = &ci;
const auto f = ci;
auto &g = ci;
cout << a << " " << b << " " << c << " " << d << " " << e << " " << g << std::endl;
a = 42;
b = 42;
c = 42;
// d = 42;
// e = 42;
// g = 42;
cout << a << " " << b << " " << c << " " << d << " " << e << " " << g << std::endl;
return 0;
}
2.35
j:int k:const int& p:const int* j2:const int k2:const int&
#include <iostream>
using namespace std;
int main() {
const int i = 42;
auto j = i;
const auto &k = i;
auto *p = &i;
const auto j2 = i, &k2 = i;
std::cout << typeid(j).name() << std::endl; // i
std::cout << typeid(k).name() << std::endl; // i
std::cout << typeid(p).name() << std::endl; // PKi
std::cout << typeid(j2).name() << std::endl; // i
std::cout << typeid(k2).name() << std::endl; // i
}
2.36
a : int 4
b : int 4
c : int 4
d : int& 4
2.37
a : int 3 // 注意decltype不会实际执行表达式
b : int 4
c : int 3
d : int& 3
2.38
auto 和 decltype 的区别主要有三方面:
auto 类型说明符用编译器计算变量的初始值来推断其类型,而 decltype 虽然也让编译器分析表达式并得到它的类型,但是不实际计算表达式的值。
编译器推断出来的 auto 类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。例如,auto 一般会忽略掉顶层 const,而把底层 const 保留下来。与之相反,decltype 会保留变量的顶层 const。
与 auto 不同,decltype 的结果类型与表达式密切相关,如果变量名加上了一对括号,则得到的类型与不加括号时会有不同。如果 decltype 使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,则编译器将推断得到引用类型。
2.39
struct Foo {}
int main() {
return 0;
}
报错信息:
test.cc:1:14: error: expected ‘;’ after struct definition
1 | struct Foo {}
| ^
| ;
2.40
struct Sales_data {
string bookNo; // 书籍编号
unsigned units_sold = 0; // 销售量
double cost_price = 0.0; // 成本价
double sell_price = 0.0; // 销售价
double revenue = 0.0; // 收益
};
2.41 2.42
略,感觉没啥好写的。可以看下面的。
https://github.com/jaege/Cpp-Primer-5th-Exercises/blob/master/ch2/2.41.cpp
与表达式密切相关,如果变量名加上了一对括号,则得到的类型与不加括号时会有不同。如果 decltype 使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,则编译器将推断得到引用类型。
##### 2.39
```cpp
struct Foo {}
int main() {
return 0;
}
报错信息:
test.cc:1:14: error: expected ‘;’ after struct definition
1 | struct Foo {}
| ^
| ;
2.40
struct Sales_data {
string bookNo; // 书籍编号
unsigned units_sold = 0; // 销售量
double cost_price = 0.0; // 成本价
double sell_price = 0.0; // 销售价
double revenue = 0.0; // 收益
};
2.41 2.42
略,感觉没啥好写的。可以看上面的链接以及github相关项目(上面的链接中有)。