0%

C++ Primer 第二章

基本内置类型

​ 内置类型包括算术类型和空类型

算术类型

1、算术类型分为两类:整型(包括字符和布尔类型)和浮点型。

2、布尔类型(bool):取值是真或假。

3、字符类型(char):一个char的空间应确保可以存放机器基本字符集中任意字符对应的数字值,即一个char的大小和一个机器字节一样(8位)。

4、短整型(short):16位

5、整型(int):16位

6、长整型(long);32位

7、长整型(long long):64位

8、单精度浮点类型(float):32位,保留6位有效数字

9、双精度浮点类型(double):64位,保留10位有效数字

10、扩展精度浮点类型(long double):96或128位,保留10位有效数字

带符号与无符号类型

​ 除去布尔类型和扩展的字符型之外,其他整型可以划分为带符号的(signed)无符号类型(unsigned) 两种,带符号类型可以表示正数、负数或0,无符号类型仅能表示大于等于0的值。

​ 类型int 、short 、 long 和 long long 都是带符号的,通过在这些类型名前添加unsigned就可以得到无符号类型,

例如 unsigned long 。类型 unsigned int 可以缩写为 unsigned 。

特殊的char

​ 与其他整型不同,字符型被分为了三种:char 、 signed char 和 unsigned char。特别注意的是:类型 cahr 和 类型 signed char 并不一样。尽管字符型有三种,但是字符型的表现形式却只有两种: 带符号和无符号的。类型char实际上会表现为上述两种形式的一种 ,具体是哪种由编译器决定。

​ 无符号类型中所有比特都用来存储值,例如,8比特的unsigned char 可以表示0至255区间内的值。

​ 但是约定了在表示范围内正值和负值的最应该平衡。因此,8比特的signed char 理论上应该可以表示-127至127区间内的值,大多数现代计算器实际的表示范围定位 -128 至 127。可以理解为:因为把0划分到了无符号类型,所以0+127 = 128.

看一段代码:

1
2
unsigned char c = -1;
signed char c2 = 256;

(均假设cahr占8比特)这两个语句的运行结果是:

c的值为255,

c2的值是未定义的。

类型转换

  • 当我们把一个非布尔类型的算术值赋给布尔类型时,初始值为o则结果为false,否则结果为true。
  • 当我们把一个布尔值赋给非布尔类型时,初始值为false 则结果为o,初始值为true则结果为1。
  • 当我们把一个浮点数赋给整数类型时,进行了近似处理。结果值将仅保留浮点数中小数点之前的部分。
  • 当我们把一个整数值赋给浮点类型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失。

结论:

1.当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。例如,8比特大小的unsigned char 可以表示0至255区间内的值,如果我们赋了一个区间意外的值,则实际的结果是该值对256取模后所得的余数。因此,把-1赋给8比特大小的unsigned char 所得的结果是255.

2.当我们赋给符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据。

字面值常量

字面值常量(literal constant),“字面值”是指只能用它的值称呼它,“常量”是指其值不能修改。每个字面值都有相应的类型,3.14是double型,2是int型。只有内置类型存在字面值。

1. 整形字面值规则

整形字面值常量可以用十进制、八进制、十六进制表示。

20 // dec

024 // oct(以0开头)

0x14 // hex(以0x或0X开头)

整形字面值常量的类型默认为int或long,其值适合int就是int类型,比int大就是long类型。在数值后加L或l(小写字母l容易与数字1混淆,建议用L)可以指定为long,加U或u指定为unsigned类型,加UL或LU定义为unsigned long类型。没有short类型的字面值常量。

2. 浮点字面值规则

可以用十进制或科学计数法(指数用E或e)表示,默认为double,在数值后加F或f表示单精度,加L或l表示扩展精度。

3.14159F .001f 12.345L 0.

3.14159E0f 1E-3F 1.2345E1L 0e0

3. 布尔字面值和字符字面值

布尔字面值:true、false。字符字面值由单引号定义:’2’(char类型)、L’a’(wchar_t类型)。

4. 非打印字符和转义序列

非打印字符和特殊字符(如单引号、双引号、反斜杠)都要写为转义字符(以反斜杠开头)。

image.png

无论是普通字符,还是非打印字符和特殊字符,都可以表示为“通用转义字符”。如八进制形式的“\7(响铃符)、\12(换行符)、\0(空字符)、\62(数字2)”(可以对照ASCII码表看一下)。“通用转义字符”也可以用十六进制表示(\xddd)。

5. 字符串字面值

需要用双引号括起来,非打印字符写要转义,编译器会自动在末尾添加一个空字符。

1
2
3
4
“hello c++”       // simple string literal
“” // empty string literal
“\n hello \t c++” // string literal using newlines and tabs
L”hello c++” // a wide string literal

6. 字符串字面值的连接

两个相邻的,仅由空格、制表符、换行符分隔的字符串字面值,将连接在一起。

1
2
3
std::cout << “a multi-line “ “string literal “
using concatenation”
<< std::endl

输出:a multi-line string literal using concatenation

7 多行字面值

可以使用反斜杠,将多行内容当作同一行处理。

1
// multiline string literal``std::cout << “a multi-line \``string literal \``using` `a backslash”``   ``<< std::endl;

反斜杠必须是行尾字符,后面不能有注释或空格。后继行行首的任何空格和制表符都是字符串字面值的一部分。

变量

​ 变量类型决定所占空间的大小的布局方式以及所能参与的运算,变量与对象可互换。

变量的定义

​ 命名空间 : : 类型说明符 空格 一个或多个变量名(中间以逗号分隔,以分号结束),可同时附初始值。

列表初始化

1
2
3
4
int unit = 0;
int unit = {0};
int unit{0};
int unit(0);

以上效相同,但2、3无法转换类型、因为存在丢失信息风险。

默认初始化

​ 定义于函数体内的内置类型的对象如果没有初始化,则其值未定义。类的对象如果没有显式地初始化,则其值由类确定。

变量声明和定义的关系

​ 由于C++支持分离式编译,一个变量可被多个文件使用,则可使用变量名前加extern

extern int i

若给extern变量赋予初始值,则关键字失效。

静态类型

​ · C+是一种静态类型(statically typed)语言,其含义是在编译阶段检查类型。其中,检查类型的过程称为类型检查(type checking)。
我们已经知道,对象的类型决定了对象所能参与的运算。在C++语言中,编译器负责检查数据类型是否支持要执行的运算,如果试图执行类型不支持的运算,编译器将报错并且不会生成可执行文件。

标识符

​ 用户自定义的标识符中不能连续出现两个下画线,也不能以下画线紧连大写字母开头。此外,定义在函数体外的标识符不能以下画线开头。

名字的作用域

**全局作用域( global scope)**。一旦声明之后,全局作用域内的名字在整个程序的范围内都可使用。

**块作用域(block scope)**。只在自己所声明区域可用。

复合类型

​ 一条声明语句由一个基本数据类型(base type)和紧随其后的一个声明符( declarator)(变量名)列表组成。每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。

引用

​ 引用并非对象,它只是已经存在对象的另一个名字,它出生就必须和初始值绑在一起,永不分离!

引用的定义

int i = 1024; int &i1 = i;

指针

​ 与引用类似,指针也实现了对其他对象的间接访问。然而指针与引用的不同点如下

  • 指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。
  • 指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。

指针的定义

int i = 1024; int *p = i;

指针值

指针的值(即地址)应属下列4种状态之一:

  • 指向一个对象。
  • 指向紧邻对象所占空间的下一个位置。
  • 空指针,意味着指针没有指向任何对象。
  • 无效指针,也就是上述情况之外的其他值。

使用解引用符*可访问所指对象,仅适用于状态1。

空指针

1
2
3
4
5
int *p1 = nullptr;
int *p2 = 0;
int *p3 = NULL;
int z = 0;
int *p4 = z;

p1,p2,p3都定义了一个空指针,但p4并不是空指针。

赋值和指针

1
2
3
4
5
int i = 0
int *p = nullptr;
int *p1 = 0;
*p = 100;
p = p1;

赋值改变的永远是等号左侧的对象。则第4行为改指向对象,第5行改指针。

void 指针*

​ 它可以指向任意类型,但也无法进行大部分操作,如输入、输出、赋给另一个void* 指针且不能直接操作所指对象。以它的视角,它只知道自己指向了一块内存空间。

指向指针的指针

int *p1 = nullptr; int **p2 = p1;

指向指针的引用

int *p = nullptr; int *&r = p;

要理解r的类型到底是什么,最简单的办法是从右向左阅读r的定义。离变量名最近的符号(此例中是&r的符号&)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针,则r为p的别名。

思考:既然有指向指针的指针,那么有引用的引用吗?

答案:[C++ 是否能够定义引用的引用?]zhihu.com/question/28023545

const限定符

const int i = 520;

这样定义一个变量后,任何试图改变i的值的操作都会报错,且const必须附予初始值。

int j = i;

i的常量特征仅仅只在执行改变i的才会发挥作用。仍可进行拷贝,运算等,一旦拷贝完成,新对象与原来的对象没什么关系了。

const的引用

把引用绑定到const对象上,称为对常量的引用

1
2
3
const int ci = 1024;
const int &r1 = ci; //正确
int &r2 = ci; //错误

初始化和对const的引用

1
2
3
4
5
int ci = 24;
const &r1 = ci;
const &r2 = 42;
const &r3 = r1*2; //虽然不可以定义引用的引用但是,此句只是引用了48这个常量而已
int &r4 = r1 * 2; //此句错误,r4是一个普通的非常量引用

小结:

  • 对于const引用一个非const对象,书中解释是可以绑定,但不允许通过此引用去修改它的值。这里我的理解是,既然不能通过自身修改,也能通过其他途径修改,那么他们就没有任何联系了,所以我理解为这里const仅仅只是引用了该对象的值而已。所以解释r4的错误应该是:r4引用了一个常量。

指针和const

与引用一样,也可以令指针指向常量或非常量。类似于常量引用,**指向常量的指针(pointer to const)**不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针:

指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定为常量。**常量指针(const pointer)*必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。把放在const关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味,即不变的是指针本身的值而非指向的那个值:

顶层const

对于一般变量而言没有顶层const和底层const区别,但一般算作顶层const。对于符合变量却有不同。

区别:

  • 指向常量的指针:代表 不能改变其指向内容的指针。声明时const可以放在类型名前后都可,拿int类型来说,声明时:const int和int const 是等价的。声明指向常量的指针也就是 底层const
  • 指针常量:代表指针本身是常量,声明时必须初始化,之后它存储的地址值就不能再改变。声明时const必须放在指针符号后面,即:const 。声明常量指针就是顶层const
  • 顶层const和底层const很简单, 一个指针本身添加const限定符就是顶层const,而指针所指的对象添加const限定符就是底层const。

作用:

  • 执行对象拷贝时有限制,常量的底层const不能赋值给非常量的底层const。也就是说,你只要能正确区分顶层const和底层const,你就能避免这样的赋值错误。

  • 使用命名的强制类型转换函数const_cast时,需要能够分辨底层const和顶层const,因为const_cast只能改变运算对象的底层const。

[练习一下,const int constconst* pppi 是顶层const还是底层const?

答案当然是底层const,因为int前面const限定符,而最后一个*后面没有const限定符。看最后一个例子:

1
2
3
4
5
const int a = 1;  
//int * pi = &a; //错误,&a是底层const,不能赋值给非底层const
const int *pi = &a; //正确,&a是底层const,可以赋值给底层const
const int *const *const ppi = &pi //即是底层const,也是顶层const
const int *const *const *pppi = &ppi; //底层const

[参考原文](18条消息) C++的顶层const和底层const的理解_冬之晓-CSDN博客_顶层const和底层const

constexpr和常量表达式

常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式。显然,字面值属于常量表达式,用常量表达式初始化的 const对象也是常量表达式。

1
2
int ci = 24;
const int r = ci;//这里并不属于常量表达式

constrxpr变量(C++ 11)

将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化:

1
2
3
constexpr int mf = 20;
constexpr int li = mf + 1;//这些都是常量表达式
constexpr int sz = size();//只有当size是一个constexpr函数才是正确的声明

字面值类型

  • 算数类型、引用、指针都属于字面值类型。

  • 自定义类、IO库、string类型不属于字面值类型。

尽管指针和引用都能定义成constexpr,但它们的初始值却受到严格限制。一个constexpr指针的初始值必须是nullptr或者o,或者是存储于某个固定地址中的对象。

  • 函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr指针不能指向这样的变量。

  • 相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始化 constexpr指针。

允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址。因此,constexpr引用能绑定到这样的变量上,constexpr 指针也能指向这样的变量。

指针和constptr

1
2
const int *p = nullptr;//p是指向常量整数的指针
constexpr int *q = nullptr;//q是指向整数的常量指针

处理类型

类型别名

它是一个名字,它是某种类型的同义词。使用类型别名有很多好处,它让复杂的类型名字变得简单明了、易于理解和使用,还有助于程序员清楚地知道使用该类型的真实目的。

1
2
3
typedef double wages;//wage是double的同义词
typedef wages base, *p;//base是double的同义词、p是double*的同义词
using SI = Sale_item;//C++ 11中一种新的方法,效果一样

指针、常量和类型别名

1
2
3
typedef char *pst;  //这里把char*看作一个整体,则pst替代的是char*
const pst cstr = 0; //cstr是指向char的常量指针
const pst *ps;//ps是一个指针,指向char的常量指针。就等于const char **ps

这里比较绕,首先pst是char*的别名,先不论pst看第二行代码,可知cstr是一个不能改变的值,再用pst替换这个值,就是不能改变指向char的指针。再不论pst看第三行,ps是一个指向常量的指针,用pst替换掉这个量,就是一个指向char的常量指针。

auto类型说明符(C++ 11)

C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。和原来那些只对应一种特定类型的说明符(比如 double)不同,auto 让编译器通过初始值来推算变量的类型。显然,auto定义的变量必须有初始值:

1
2
3
auto item = val1 + val2;
auto i = 0, *p = &i;//类型一致可以一起定义
auto sz = 0, pi = 3.14;//错误若类型不一致不可一起定义

符合类型’常量和auto

  • 当引用被当作初始值,参与的是引用的对象
  • auto一般会忽略掉顶层const,同时底层const会保留下来
1
2
3
4
5
6
7
8
9
10
11
int i = 0;
const int ci = i, &cr = ci;
auto b = ci; //b为整数(顶层const忽略)
auto c = cr; //c为整数(一样忽略顶层const)
auto d = &i; //d为指向整形的指针
auto e = &ci; //e是指向整数常量的指针(这里属于底层const,保留)
//如果需要auto是顶层const则可以
const auto f = ci;//推演为int,但f为const int
auto &g = ci;//推演为int,可定义引用
auto &h = 42;//非常量引用不可以绑定字面值
const auto &j = 42;//常量引用可以绑定字面值

decltype类型指示符(C++ 11)

它的作用是选择并返回操作数的数据类型,此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。

decltype(f()) sum = x;//sum的类型就是函数f返回的类型

编译器并不实际调用函数f,而是使用当调用发生时f的返回值类型作为sum的类型。换句话说,编译器为sum指定的类型是什么呢?就是假如f被调用的话将会返回的那个类型。

1
2
3
4
cosnt int ci = 0, &cj = ci;
decltype(ci) x = 0;//x类型是const int
decltype(cj) y = x;//y的类型是const int&,则y绑定了x
decltype(cj) z;//z类型是const int&,必须初始化

decltype和引用

1
2
3
4
int i = 42, *p = &i, &r = i;
decltype(r+0) b;//正确,加法得到的结果是int,因此b为int类型
decltype(*p);//错误,解引用得到的是int&,而非int,需要初始化
decltype(r) c;//错误,这里没有用加法,得到的是引用类型,需初始化

使用+0的操作可以避免变成引用

1
2
decltype((i)) d;//d为引用
decltype(i) e;//为int

自定义的数据结构

从最基本的层面理解,数据结构是把一组相关的数据元素组织起来然后使用它们的策略和方法。

定义Sales_data类型

1
2
3
4
struct Sales_data {/*...*/} accum, trans, *salesptr;
//两种定义相同,但下面一种更好
struct Sales_data {/*...*/};
Sales_data accum, trans, *salesptr;

类体定义成员,这个类只有数据成员。类体定义完成后相当与一个类型。

C++11新标准规定,可以为数据成员提供一个**类内初始值(in-class initializer)**。创建对象时,类内初始值将用于初始化数据成员。没有初始值的成员将被默认初始化。

1
2
3
4
5
struct sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};

使用Sale_data类

总体轮廓

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <string>
#include "sales_data.h"
int main (){
sales_data datal, data2;
//读入datal和data2的代码
//检查datal 和data2的工SBN是否相同的代码
//如果相同,求datal和data2的总和
}

读入数据并处理

1
2
3
4
5
double price = 0; //书的单价,用于计算销售收入
//读入第1笔交易:ISBN、销售数量、单价
std: :cin >> data1.bookNo >> datal.units_sold >> price;
//计算销售收入
data1.revenue = data1.units_sold *price;

输出和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (data1 . bookNo m= data2.bookNo){
unsigned totalCnt = data1.units_sold + data2.units_sold;
double totalRevenue = data1.revenue + data2.revenue;
//输出:ISBN、总销售量、总销售额、平均价格
std::cout << data1.bookNo<<" " << totalCnt<<" " << totalRevenue << "";
if (totalcnt != o)
std:: cout << totalRevenue/totalCnt << std: :endl;
else
std: :cout <<" (no sales) " << std: :endl;
return 0;//标示成功
}
else {
//两笔交易的ISBN不一样
std: :cerr <<"Data must refer to the same ISBN" <<std: :endl;
return -l;//标示失败
}

编写自己的头文件

头文件一旦改变,相关源文件必须重新编译以获取更新过的声明。

预处理概述

确保头文件多次包含仍能安全工作的常用技术是预处理器(preprocessor),它由C++语言从C语言继承而来。预处理器是在编译之前执行的一段程序,可以部分地改变我们所写的程序。例如#include,当预处理器看到#include标记时就会用指定的头文件的内容代替#include。
C++程序还会用到的一项预处理功能是头文件
保护符(header guard),头文件保护符依赖于预处理变量。预处理变量有两种状态:已定义和未定义。#define
指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:#ifdef当且仅当变量已定义时为真,**#ifndef当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到#endif**指令为止。

1
2
3
4
5
6
7
8
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
struct sales_data {
std : : string bookNo ;
unsigned units_sold = 0;double revenue = 0.0;
};
#endif

本章小结

类型规定了其对象的存储要求和所能执行的操作。C++语言提供了一套基础内置类型,如int和char等,这些类型与实现它们的机器硬件密切相关。类型分为非常量和常量,一个常量对象必须初始化,而且一旦初始化其值就不能再改变。此外,还可以定义复合类型,如指针和引用等。复合类型的定义以其他类型为基础。
C++语言允许用户以类的形式自定义类型。C++库通过类提供了一套高级抽象类型,如输入输出和string等。