C++ primer notes: variables and basic types and exercises

Basic built-in types

Including arithmetic types and empty types . Arithmetic types include characters, integers, Boolean values, and floating-point numbers. The empty type does not correspond to a specific reference and is used for special occasions.

arithmetic type

Arithmetic types are divided into two categories: **integers (including characters and Boolean types)** and floating-point types. The size of the arithmetic type is different on different machines, the following table is the minimum size value stipulated by the C++ standard.

type meaning smallest size
bool Boolean type undefined
char character 8 bits
wchar_t wide character 16 bits
char16_t Unicode characters 16 bits
char32_t Unicode characters 32 bit
short short integer 16 bits
int integer 16 bits
long long integer 32 bit
long long long integer 64 bit
float single precision floating point 6 significant figures
double double precision floating point 10 significant figures
long double Extended Precision Floating Point 10 significant figures

 C++ provides several character types, the basic being char, whose size is equal to one machine byte. Other character types are used for extended character sets, wchar_t is used to store any character in the maximum extended character set of the machine, char16_t and char32_t serve for Unicode character sets.
 C++ stipulates that an int is at least as large as a short, a long is at least as large as an int, and a long long is at least as large as a long. Among them, long long is newly defined in C++11.
 Usually, float is represented by 1 word (32 bits), double is represented by 2 words (64 bits), and long double is represented by 3 or 4 words.

Signed and unsigned types
 In addition to Boolean and extended character types, other integer types are divided into two types : signed and unsigned . Signed types can represent integers, negative numbers, or 0, while unsigned types can only represent values ​​greater than 0.
 The types int, short, long, and long long are signed integers, and unsigned types can be obtained by adding unsigned in front of them.
 Character types are divided into three types: char, signed char, and unsigned. Type char is interpreted by the compiler as signed char or unsigned char.

How to choose the type

- When the value cannot be negative, use the unsigned type.
- Performs arithmetic operations with int. If the value exceeds the representation range of int, choose long long.
- Do not use char or bool in arithmetic expressions.
- Double is used to perform floating-point arithmetic.

Exercise 2.1: What is the difference between the types int, long, long long, and short? What is the difference between unsigned and signed types? What is the difference between float and double?
  • The minimum sizes of the three integer types are different, int is 16 bits, long is 16 bits, and long long is 32 bits. The three sizes may become larger on different machines and compilers. Different types occupy different digits and represent different ranges.
  • Unsigned types cannot represent negative numbers, and signed types can represent positive numbers, 0, and negative numbers.
  • float is a single precision floating point number and double is a double precision floating point number. Usually float occupies 32 bits in memory, and double occupies 64 bits. The range and precision of the numbers that can be represented by the two are also different.
    Exercise 2.2: When calculating a mortgage loan, which data types should be chosen for the interest rate, principal, and payment?
    The three are generally decimals, and using integers will cause the part after the decimal point to be lost, so floating-point numbers should be selected. And because the calculation cost of float and double is similar, and the range and precision of double are larger than that of float; although long double has a larger range and precision, the calculation cost is too high. So choose double.

type conversion

Make the following assignments:

bool b = 42;                 //b 为真
int i = b;					 // i 的值为1
i = 3.14;                    // i 的值为3
double pi = i;               // pi 的值为3.0//
unsigned char c = -1;        // 假设 char 占 8 比特,c 的值为255
signed char c2 = 256;        //假设 char 占 8 比特,c2 的值是未定义的

When performing mandatory type conversion on variables, the following rules are generally followed:

  • When an arithmetic value of a non-Boolean type is assigned to a Boolean type, the result is false if the initial value is 0, otherwise the result is true.
  • When a Boolean value is assigned to a non-Boolean type, the initial value is false and the result is 0, and the initial value is true and the result is 1.
  • Approximate processing is performed when floating-point numbers are assigned to integer types. Only the part before the decimal point in the floating point number is kept.
  • When an integer type is assigned to a floating point type, the decimal part is recorded as 0. If the space occupied by the integer exceeds the capacity of the floating-point type, precision may be lost.
  • When assigning an unsigned type a value beyond its range, the result is the remainder of the initial value modulo the value represented by the unsigned type.
  • When a signed type is assigned a value outside its expressive range, the result is undefined.

When the value of one arithmetic type is used in the program and the value of another type is required, the compiler will also perform the above-mentioned type conversion. For example:

int i = 42;
if(i)
	i = 0;

If i is 0, the condition evaluates to false; if i is nonzero, it evaluates to true.

expressions with unsigned types

unsigned u = 10;
int i = -42;
std::cout << i + i << std::endl;  //输出-84
std::cout << i + u << std::endl;  //如果 int 占 32 位,输出4294967264

The first expression is correct, but the output of the second expression is wrong, because -42 will be converted to an unsigned number during operation, and its value is the value after its modulo.

unsigned u1 = 42, u2 = 10;
std::cout << u1 - u2 << std::endl;  //输出32
std::cout << u2 - u1 << std::endl;  //输出4294967264

The first expression is correct, but the second is wrong for the same reason as the previous code snippet.

So: never mix signed and unsigned types

Exercise 2.3: Read the program and write the result.
unsigned u = 10, u2 = 42;
std::cout << u2 - u << std::endl;
std::cout << u - u2 << 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;
32 4294967264 32 -32 0 0 Exercise 2.4: Write a program to check that your estimates are correct. If not, read this section carefully until you understand the problem.

insert image description here

literal constant

42 Such values ​​that can be seen at a glance are called literal constants , each literal constant corresponds to a data type, and the form and value of the literal constant determine its data type.

Integer and floating point literals

Integers can be written as decimal, octal, or hexadecimal numbers. An integer starting with 0 represents an octal number, and an integer starting with 0x represents a hexadecimal number.
The type of a decimal literal is the smallest of int, long, and long long that can hold its value. The type of an octal and hexadecimal literal is the smallest of int, unsigned int, long, unsigned long, long long, and unsigned long long that can hold its value. It is an error if the largest of the literal associated types does not fit. short does not take a literal value.

Floating-point literals are exponents expressed as decimals or in scientific notation, where the exponent part is identified by E or e. Floating-point literals default to double.

Character and String Literals
A character enclosed in single quotes is called a char literal, and multiple characters enclosed in double quotes form a string literal.
Strings are actually arrays of constant characters. The compiler adds a null character ('\0') to the end of each string, so the actual length of the string literal is one more than the content.
Two string literals are effectively a whole if they are located adjacent to each other and separated only by spaces, indentation, and newlines.

//多行书写的字符串字面值
std::cout << "a really, really long string literal"
		"that spans two lines" <

Escape sequences
There are two types of characters that programmers cannot use directly: one is non-printable characters, such as backspace or other control characters, because they are invisible; the other is characters with special meaning in C++ (single quote, double quotes, question marks, backslashes). In these cases , escape sequences are used . The escape sequences specified by C++ include:

Line break: \
nHorizontal tab:\
tAlarm:\
aVertical tab:\vBackspace:
\bDouble quote
: "
Backslash: \
question mark: ?
Single quote: '
Carriage return: \r
Feed character: \f

There are also generalized escape sequences of the form \x followed by 1 or more hexadecimal digits, or \ followed by 1, 2, or 3 octal digits, assuming Latin-1 Character set, here are some examples:
\7 (bell) \12 (newline) \40 (space) \0 (null character) \115 (character M) \x4d (character M)

Specifying the Type of Literals The default types
of integer, float, and character literals can be changed by adding the prefixes and suffixes listed in the following tables.

Character and string literals
prefix meaning type
u Unicode 16 characters char16_t
U Unicode 32 characters char32_t
L wide character wchar_t
u8 UTF-8 (only for string literals) char
integer literal
suffix Minimum match type
u or U unsigned
l or L long
ll or LL long long
floating point literal
suffix type
f or F float
l or L long double
L'a' //宽字符型字面值,类型是 wchar_t
u8"hi!" //utf-8 字符串字面值(utf-8 用 8 位编码一个 Unicode 字符)
42ULL   //无符号长长整型字面值,类型是 unsigned long long 
1E-3F   //科学计数法表示的单精度浮点型字面值,类型是float
3.14159L//扩展精度浮点数字面值,类型是 long double

布尔字面值和指针字面值
true 和 false 是布尔类型的字面值,nullptr 是指针字面值。

练习 2.5:指出下述字面值的数据类型并说明每一组内几种字面值的区别:

1. ‘a’, L’a’, “a”, L"a"
2.10, 10u, 10L, 10uL, 012, 0xC
3. 3.14,3.14f,3.14L
4. 10,10u,10.,10e-2

答:
1. ‘a’:字符型字面值,L’a’:宽字符型字面值, “a”:字符串字面值, L"a":宽字符串字面值
2. 10:整型字面值, 10u:无符号整型字面值,类型是unsigned, 10L:长整型字面值, 10uL:无符号长整型字面值, 012:八进制表示的整数, 0xC:是一个十六进制数。
3. 3.14:浮点型字面值,3.14f:float类型的单精度浮点数,3.14L:long double类型的扩展精度浮点数。
4. 10:整数,10u:无符号整数,10.:浮点数,10e-2:科学计数法表示的浮点数。

练习 2.6:下面两组定义是否有区别,如果有,则叙述:

int month = 9, day = 7;
int month = 09, day = 07;

有区别,第一组定义是用十进制的字面值给整型变量赋值,month 和 day 的值分别为 9 和 7;第二组定义是用八进制整数给整型变量赋值,但是八进制表示中不会出现大于等于 8 的数,所以第二组赋值会报错。

练习 2.7:下述字面值表示何种含义?他们各自的数据类型是什么?
(a)“Who goes with F\145rgus?\012”
(b) 3.14elL
(c)1024f
(d) 3.14L
答:
(a)\145表示字符 e,\012 表示换行符,数据类型是字符串。
(b) 科学计数法表示的扩展精度浮点数。
(c)试图定义单精度浮点数,但是整数 1024 后直接跟了 f,编译器将报错;改写成 1024.f就好了。
(d) 扩展精度浮点数,类型是 long double。

练习 2.8:请利用转义序列编写一段程序,要求先输出 2M,然后转到新的一行。修改程序使其先输出 2,然后输出制表符,再输出 M,最后转到新一行。

cout << "2\115\12";
cout << "2\t\115\12";

变量

变量定义

变量定义的基本形式:首先是类型说明符,随后紧跟由一个或多个变量名组成的列表,其中变量名以逗号相隔,最后以分号结束:

int sum = 0, value, units_sold = 0;
std::string book("0-201-23049-X");

book 的定义用到了库类型 std::string,上例中把字面值拷贝给 string 对象。

术语:何为对象?
对象指一块能存储数据并具有某种类型的内存空间。

初始值
当对象在创建时获得了一个特定的值,这个对象就被初始化了。

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

列表初始化
C++语言定义了初始化的几种形式。例如要定义一个名为 units_sold 的变量并初始化为 0,以下 4 条语句都可以:

int units_sold = 0;
int units_sold = {
    
    0};
int units_sold{
    
    0};
int units_sold(0);

第二、三种赋值方法称为列表初始化
当用于内置类型的变量时,这种初始化形式有一个重要特点:如果使用列表初始化且初始值存在丢失信息的风险,则编译器将报错:

long double ld = 3.1415926536int a{
    
    ld}, b = {
    
    ld};           //错误:转换未执行,存在丢失信息的风险。
int c(ld), d = ld;             //正确:转换执行,丢失部分信息。

默认初始化

定义变量时没有指定初值,则变量被默认初始化
如果是内置类型未被显示初始化,它的值由定义的位置决定。定义于函数之外的变量初始化为 0。定义在函数体内部的内置类型变量将不被初始化。一个未被初始化的变量值是未定义的。

练习 2.9:解释下列定义的含义。对于非法的定义,请说明错在何处并将其改正。

(a)std::cin >> int input_value;
(b)int i = {3.14};
(c)double salary = wage = 9999.99;
(d)int i = 3.14;
答:
(a)std::cin 后应该跟变量名称,不应该是变量的定义。改正后:int input_value; std::cin >> input_value;
(b)使用列表初始化将浮点数赋初始化整型对象,因存在信息丢失的风险,所以会警告。
(c)该语句试图将 9999.99 初始化 salary 和 wage,但是在声明多个变量时要用逗号隔开,不能直接用等号相连。改正后:double salary, wage; salary = wage = 9999.99;
(d)和 b 一样,会造成小数部分丢失。

练习 2.10:下列变量的初值分别是什么?

std::string global_str;
int global_int;
int main()
{
    
    
 	int local_int; 
 	std::string local_str;
}
答:string 不是内置类型,所以 global_str 与 local_str 都被初始化为空串。int 为内置类型,函数范围外的 global_int 被初始化为 0,函数内的 local_int 不会被初始化。

提示:未初始化变量引发运行时故障
使用未初始化的变量会带来无法预计的后果。有可能一访问此类对象就会报错,但也有可能产生错误的结果,无法把握。
所以要初始化每一个内置类型的变量。

变量声明和定义的关系

声明使得名字为程序所知,定义负责创建于名字关联的实体。
如果想声明变量而非定义它,就在变量名前添加关键字 extern,而且不要显示初始化变量,否则就称为了定义:

int j;                   //声明并定义 j
extern int i;            //声明 i 而非定义 i
extern double pi = 3.14  //定义 pi

在函数体内部,初始化一个由 extern 关键字标记的变量将引发错误。变量只能被定义一次,但是可以被多次声明。

练习 2.11:指出下面的语句是声明还是定义:

(a)extern int ix = 1024;
(b)int iy;
(c)extern int iz;

答:
(a)声明变量的同时进行了显式初始化,所以是定义。
(b)定义。
(c)使用 extern 关键字进行了声明。

关键概念:静态类型

C++是一种静态类型语言,在编译阶段检查类型,这个过程称为类型检查。编译器检查数据类型是否支持要执行的运算,如果试图执行类型不支持的运算,编译器将报错,且不会生成可执行文件。

标识符

C++的标识符由字母、数字和下划线组成,且必须以字母或下划线开头,对长度没有限制,但对大小写敏感。
C++ 关键字不能用作标识符。C++也为标准库保留了名字,用户自定义的标识符中不能连续出现两个下划线,也不能下划线紧连大写字母开头,在函数体外的标识符不能以下划线开头。

变量命名规范

  • 标识符体现实际含义。
  • 变量名一般用小写字母,用 index ,不要使用 Index 或 INDEX。
  • 用户自定义的类名一般以大写字母开头。
  • 如果标识符由多个单词组成,则单词间应有明显区分。

练习2.12:请指出下面的名字中哪些是非法的?
(a)int double = 3.14;
(b)int _;
(c)int catch-22;
(d)int 1_or_2 = 1;
(e)double Double = 3.14;

答:
(a)非法的,使用了关键字 double。
(b)合法。
(c)非法,标识符中不能出现 - 。
(d)非法,不能以数字开头。
(e)合法。

名字的作用域

**作用域(scope)**即名字的有效区域。同一名字在不同的作用域中可能指向不同的实体(变量、函数、类型等)。

有下面示例:

#include <iostream>
int main()
{
    
    
 	int sum = 0;
 	// sum values from 1 through 10 inclusive
 	for (int val = 1; val <= 10; ++val)
 		sum += val; // equivalent to sum = sum + val
 	std::cout << "Sum of 1 to 10 inclusive is " << sum << std::endl; 
 	return 0;
}

里面有 3 个名字: main、sum 和 val,main 在所有花括号外,拥有全局作用域,声明后在整个程序范围内都能使用,名字 sum 定义于 main 函数所限定的作用域之内,从声明 sum 开始到 main 函数结束都能访问,出了 main 后就不能访问了,val 定义于 for 语句内,在 for 语句之内可以访问 val,出了 for 循环就不能访问,val 与 sum 都具有块作用域。

建议:第一次使用变量时再定义它

嵌套的作用域
作用域能彼此包含,被嵌套的作用域称为内层作用域(inner scope),包含着别的作用域的作用域称为外层作用域(outer scope)

#include <iostream>
// Program for illustration purposes only: It is bad style for a function
// to use a global variable and also define a local variable with the same name
int reused = 42; // reused has global scope
int main()
{
    
    
 	int unique = 0; // unique has block scope
 	// output #1: uses global reused; prints 42 0
 	std::cout << reused << " " << unique << std::endl;
 	int reused = 0; // new, local object named reused hides global reused
 	// output #2: uses local reused; prints 0 0
 	std::cout << reused << " " << unique << std::endl;
 	// output #3: explicitly requests the global reused; prints 42 0
 	std::cout << ::reused << " " << unique << std::endl; return 0;
}

第一个输出,使用全局作用域中定义的名字 reused。
第二个输出,使用块作用域中的名字 reused。
第三个输出,使用作用域操作符覆盖默认的作用域,访问了全局作用域中的reused。

练习 2.13:下面程序中 j 的值是多少?

int i = 42;
int main()
{
    
    
	int i = 100; int j = i;
}

答:内层作用域的值对外层作用域的值进行了覆盖,j 所得的值是 内层作用域中 i 的值 100。

练习 2.14:下面的程序合法吗?如果合法,将输出什么?

int i = 100, sum = 0;
for (int i = 0; i != 10; ++i)
 sum += i;
std::cout << i << " " << sum << std::endl;
答:合法的。100 45。 循环内部定义的 i 是内层作用域中的,从 0 递增到 9,所以 sum 是 45。循环外部 for 中的 i 失效,输出的 i 是外层作用域中的,值为 100。

复合类型

复合类型(compound type) 是基于其他类型定义的类型。这里介绍引用和数组。

声明语句由一个基本数据类型和后面一个声明符列表组成。每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。

引用

C++11 中新添了右值引用,这种引用主要用于内置类。当我们使用 “引用” 时,指的是左值引用
引用(reference) 为对象起了另外一个名字,引用类型 引用(refers to)另外一种类型。通常将声明符写成 &d 的形式来定义引用类型,其中 d 是声明的变量名:

int ival = 1024;
int &refVal = ival;     //refVal 指向 ival(是 ival 的另一个名字)
int &refVal2;           //报错:引用必须被初始化。

初始化变量时,初始值拷贝到变量中。定义引用时,引用和初始值绑定(bind) 在一起,初始化完成后,引用将和它的初始化对象一直绑定,无法另引用绑定到另外一个对象,所以引用必须初始化。
引用即别名

引用不是对象,它是一个已经存在的对象的另一个名字。

对一个引用操作是对其绑定的对象操作:

refVal = 2;           //把 2 赋给 refVal 所指的对象,即 ival
int ii = refVal;      //与 ii = ival 一样

为引用赋值,实际上是把值付给了与引用绑定的对象。获取引用的值,是获取与引用绑定对象的值。以引用作为初始值,实际上是以与引用绑定的对象作为初始值:

int &refVal3 = refVal; //正确:refVal3 绑定到了与 refVal 绑定的对象上,即 ival。
int i = refVal;        //正确:i 被初始化为 ival 的值。

上面第一个语句:由于引用本身不是对象,所以不能定义引用的引用。

引用的定义

一条语句可以定义多个引用,每个引用标识符以&开头:

int i = 1024, i2 = 2048;  
int &r = i, r2 = i2;      //r是一个引用,与 i 绑定在一起
int i3 = 1024, &ri = i3;  //ri是一个引用,与 i3 绑定
int &r3 = i2, &r4 = i2;   //r3,r4 都是引用

大部分情况,所有引用的类型都要和与之绑定的对象严格匹配,而且引用只能绑定到对象上,不能与字面值或某个表达式绑定:

int &refVal4 = 10; // 错误:初始值必须是对象
double dval = 3.14;
int &refVal5 = dval; // 错误不能用字面值初始化对象

练习 2.15:下面的哪个定义是不合法的?为啥?
(a)int ival = 1.01;
(b)int &rval1 = 1.01;
(c)int &rval2 = ival;
(d)int &rval3;

答: (a)正确 (b)不合法,不能用字面值初始化对象 (c)正确 (d)不合法,引用必须被初始化。

练习 2.16:考察下面的所有赋值然后回答:哪些赋值是不合法的?为什么?哪些赋值是合法的?它们执行了什么样的操作?

int i = 0, &r1 = i;
double d = 0, &r2 = d;

(a)r2 = 3.14;
(b)r2 = r1;
(c)i = r2;
(d)r1 = d;

答:
(a)合法的,为引用赋值就是为引用绑定的对象赋值
(b)是合法的,获取引用的值就是获取引用绑定对象的值,为引用赋值就是为引用帮对的对象赋值,这个语句就是把 i 的值赋给 d。
(c)合法的,将 d 的值赋给 i。
(d)把 d 的值赋给 i。

练习 2.17:执行下面的代码将输出什么结果?

int i, &ri = i;
i = 5; ri = 10;
std::cout << i << " " << ri << std::endl;

答:输出10 10,对 ri 赋值就是对 i 赋值,输出 i 就是10,输出 ri 就是输出 i 的值 10。

指针

指针(pointer) 是指向另外一种类型的复合类型。指针也实现了对其他引用的间接访问。但是与引用有两点不用:指针是对象,允许指针赋值和拷贝,指针可以指向不同的对象;指针在定义时也无需赋值。在块作用域未初始化的指针会有一个不确定值。

将声明符写成 *d 的形式来定义指针,d 是变量名。如果在一条语句中定义了几个指针变量,每个变量前都要有 * 号:

int *ip1, *ip2; // p1 和 p2 都是整形指针。
double dp, *dp2; // dp2 是指向double的指针。

获取对象的地址
指针存放某个对象的地址,获取地址使用取地址符(&):

int ival = 42;
int *p = &ival; // p 存放变量 ival 的地址,p是指向ival的指针。

一般情况下,指针的类型都要和它所指的对象严格匹配:

double dval;
double *pd = &dval; // 正确:初始值是 double 型对象的地址。
double *pd2 = pd; // 正确:初始值是指向 double 对象的指针。
int *pi = pd; // 错误:指针 pi 的类型和 pd 的类型不匹配。
pi = &dval; // 错误:试图把 double 型对象的地址赋给 int 型指针。

指针值

指针的值有下列 4 中状态:

  1. 指向一个对象。
  2. 指向紧邻对象所占空间的下一个位置。
  3. 空指针,没有指向任何对象。
  4. 无效指针,是上述情况之外的其他值。

利用指针访问对象

使用**解引用符(*)**访问指针指向的对象。

int ival = 42;
int *p = &ival;
cout << *p;   //用符号 * 得到指针 p 所指的对象,输出 42

给解引用的结果复制,实际上就是给指针所指的对象赋值:

*p = 0; // 通过 * 获得 p 所指的对象,即可通过 p 为变量 ival 赋值。
cout << *p; 

空指针
空指针(null pointer): 不指向任何对象。通过以下方法生成空指针:

int *p1 = nullptr; 
int *p2 = 0; 
int *p3 = NULL;   //需要首先 #include cstdlib

把 int 变量直接赋给指针是错误的操作,即使 int 变量的值为 0:

int zero = 0;
pi = zero;

建议:初始化所有指针

赋值和指针

引用一旦定义,无法绑定其他对象。对指针赋新值就可以使其存放新的地址,指向新的变量:

int i = 42;
int *pi = 0; // pi is initialized but addresses no object
int *pi2 = &i; // pi2 initialized to hold the address of i
int *pi3; // if pi3 is defined inside a block, pi3 is uninitialized
pi3 = pi2; // pi3 and pi2 address the same object, e.g., i
pi2 = 0; 

这个语句改变了指针的值:

pi = &ival;

这个语句改变了指针所指的对象:

*pi = 0;

其他指针操作

只要指针拥有一个合法值,就能用在条件表达式中。如果指针的值是 0,条件取 false,值不是 0 条件取 true:

int ival = 1024;
int *pi = 0; // pi is a valid, null pointer
int *pi2 = &ival; // pi2 is a valid pointer that holds the address of ival
if (pi) // pi has value 0, so condition evaluates as false
 // ...
if (pi2) // pi2 points to ival, so it is not 0; the condition evaluates as true
 // ...

void* 指针

void* 指针可用于存放任意对象的地址。

double obj = 3.14, *pd = &obj;
// ok: void* can hold the address value of any data pointer type
void *pv = &obj; // obj can be an object of any type
pv = pd; // pv can hold a pointer to any type

练习 2.18:编写代码分别更改指针的值以及指针所指对象的值。
答:

int i = 0, i2 = 0;
int *p = &i;
p = i2;               //改变指针的值
*p = 1;               //改变指针所指的对象 i2 的值

练习 2.19:说明指针和引用的主要区别:

答:指针指向内存中的某个对象,引用绑定到内存中的某个对象,都实现了对其他变量的间接访问。
区别:
第一,指针本身就是对象,允许对指针进行赋值和拷贝,而且指针可以指向不同的对象;引用一旦初始化后就无法赋值,只能与一个对象绑定。
第二,指针无需在初始化时赋值,在全局作用域内未初始化会被赋予空指针,块作用域内会拥有不确定值;而引用必须在定义时赋值。

练习 2.20:请叙述下面这段代码的作用。

int i = 42;
int *p1 = &i;
*p1 = *p1 * *p1;

答:将 i * i 的值赋给 i。

练习 2.21:请解释下述定义。在这些定义中有非法的吗,如果有,为什么?

int i = 0;

(a)double* dp = &i;
(b)int *ip = i;
(c)int *p = &i;

答:
(a)非法,dp 是double指针,不能指向整型变量。
(b)非法,ip 是int指针,应指向 i 的地址。
(c)合法。

练习 2.22:假设 p 是一个 int 型指针,请说明下述代码的含义。

if(p) 
if(*p)

答:第一个语句中,若 p 的值是 0,即为空指针,则条件为 false。第二个语句中,若 p 所指对象的值是 0,则条件为 false。

练习 2.23:给定指针 p,你能知道它是否指向了一个合法的对象吗?如果能,叙述判断思路;不能则说明原因。

答:用 if 语句,指向不合法对象时条件则为 false。当 p 未初始化时,应将 if 语句放在 try 语句中防止出错。

练习 2.24:在下面这段代码中为什么 p 合法而 lp 非法?

int i = 42;
void *p = &i;
long *lp = &i;

答:p 是 void * 型指针,可以存放任意对象的地址;lp 是长整型指针,不能指向整型变量。

理解复合类型的声明

变量的定义包括一个基本数据类型和一组声明符。在一条定义语句中,虽然基本数据类型只有一个,但是声明符的形式可以不同。即一条定义语句可能定义出不同类型的变量:

int i = 1024, *p = &i, &r = i;

指向指针的指针

通过 * 的个数可以区分指针的级别。** 表示指向指针的指针,***表示指向指针的指针的指针。

int ival = 1024;
int *pi = &ival;
int *ppi = &pi;

解引用 int 型指针会得到一个 int 型的数,解引用指针的指针会得到一个指针:

cout << "The value of ival\n" << "direct value: " << ival 
	<< "\n" << "indirect value: " << *pi << "\n" << "doubly indirect value: " 
	<< **ppi << endl

指向指针的引用
引用本身不是一个对象,因此不能定义指向引用的指针。但是指针是对象,所以存在对指针的引用:

int i = 42;
int *p; // p is a pointer to int
int *&r = p; // r is a reference to the pointer p
r = &i; // r refers to a pointer; assigning &i to r makes p point to i
*r = 0; // dereferencing r yields i, the object to which p points; changes i
to 0

从右往左阅读 r 的定义。离变量名最近的符号 & 说明 r 是一个引用。声明符的其余部分 * 说明 r 引用的是一个指针,所以 r 引用的是一个 int 指针。

练习 2.25:说明下列变量的类型和值。
(a)int *ip, i, &r = i;
(b)int i, *ip = 0;
(c)int *ip, ip2;

答:
(a)ip 整型指针,值为 nullptr,i 是整数,值为 0,r 是 i 的引用,值为 0。
(b)i 是整数,值为 0,ip 是整形指针,值为 0。
(c)ip 是整形指针,ip2是整数。值都为0。

const 限定符

使用关键字 const 限定的变量的值不能被改变。因为const 对象创建后它的值就不能被改变,所以 const 对象一定要初始化:

const int i = get_size();     //运行时初始化
const int j = 42;             //编译时初始化
const int k;                  //错误:k 是一个未经初始化的常量

i = 42;                       //错误:试图向 const 对象写值

初始化和 const

const 对象能完成大部分非 const 类型所能参与的操作,主要不能完成的就是改变 const 对象内容的操作。
不改变 const 对象的操作中有一种是初始化,如果利用一个对象去初始化另外一个对象,则它们是不是const都可以:

int i = 42;
const int ci = i;            //正确
int j = ci;                  //正确

因为拷贝一个对象的值不会改变它,所以上面的第三条是可以的。

默认状态下,const 对象仅在文件内有效

用编译时初始化定义一个 const 对象时,编译器会将在编译过程中用到该变量的地方都替换为对应的值。
为了执行替换,编译器必须知道初始值。如果程序有多个文件,则每个用了 const 对象的文件都必须能访问到初始值,即每个文件都必须要定义这个变量。为了避免重复定义,const 对象被设定为仅在当前文档生效。

初始值不是常量表达式的 const 变量如果要在文件间共享,又不想让每个文件分别生成独立的变量。那么就可以对于 const 变量的声明还是定义都添加 extern 关键字,这样定义一次就可以了:

//在 file1.cpp 中定义
extern const int bufSize = fun();
//在file2.cpp 中声明
extern const int bufSize;   //与 file1.cpp 中的 bufSize 是一个变量

如果想在多个文件之间共享 const 对象,必须在变量的定义之前添加 extern 关键字。

练习 2.26:下面哪些句子是合法的?如果有不合法的句子,请说明为什么?
(a)const int buf;
(b)int cnt = 0;
(c)const int sz = cnt;
(d)++cnt; ++sz;

答:
(a)不合法,没有初始化
(b)合法
(c)合法
(d)不合法,sz 是常量,不能进行自加操作

const 的引用

一个引用绑定到 const 对象上,这个引用就是常量引用(reference to const)。对常量的引用不能被用来修改它所绑定的对象。:

const int ci = 1024;
const int &r1 = ci;    //正确:引用及被绑定的对象都是常量
r1 = 42;               //错误:r1 是对常量的引用,不能用过常量引用改变常量的值。
int &r2 = ci;          //错误:试图让一个非常量引用来引用一个常量对象。

常量初始化后不能再赋值,所以不能通过引用 r1 去改变 ci。所以对常量的引用也一定要是常量,因此,对 r2 的初始化是错误的。

初始化和对 const 的引用

上面提到,引用类型必须与其所引用的对象类型一致,但有两个例外,第一个就是在初始化常量引用时允许用任意表达式作为初始值,只要表达式的结果能转换成引用的类型即可。包括允许为一个常量引用板顶非常量的对象、字面值和一般表达式:

int i = 42;
const int &r1 = i;    //允许将 const int & 绑定到 int 上
const int &r2 = 42;   //正确:r2 是一个常量引用
const int &r3 = r1 * 2;//正确:r3 是一个常量引用
int &r4 = r1 * 2;     //错误:r4 是一个普通的非常量引用,需要绑定一个整型变量

为什么会有上面情况呢?

先让我们来看一下一个常量引用被绑定到另一种类型上时发生了什么:

double dval = 3.14;
const int &ri = dval;

此处虽然 dval 是一个双精度浮点数,但是 ri 却引用了一个 int 型的数。为了确保让 ri 绑定一个整数,编译器做了如下操作:

const int temp = dval;   //由双精度浮点数生成一个临时的整型常量
const int &ri = temp;    // 让 ri 绑定这个临时常量

这时 ri 绑定的就是一个**临时量(temporary)**对象。临时量对象简称临时量,就是编译器需要一个空间来暂存表达式求值结果时临时创建的一个对象。

上面如果 ri 不是常量时,上面的赋值会有什么问题呢?

如果 ri 不是常量,ri 会引用一个临时量,我们可以通过 ri 对其修改,那么我们修改的是临时量,没有任何意义,所以这种行为是非法的。

对 const 的引用可能引用一个非 const 对象

常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。因为对象也可能是个非常量,所以可以通过其他途径修改它的值:

int i = 42;
int &r1 = i; // r1 绑定到对象 i
const int &r2 = i; // r2 是 i 的常量引用,不允许通过 r2 修改 i 的值
r1 = 0; // i 为 0
r2 = 0; //错误:r2 是常量引用,不允许通过 r2 修改 i 的值

指针和 const

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

const double pi = 3.14; // pi is const; its value may not be changed
double *ptr = &pi; // error: ptr is a plain pointer
const double *cptr = &pi; // ok: cptr may point to a double that is const
*cptr = 42; // error: cannot assign to *cptr

上面提到,指针的类型必须与其所指的对象类型一致,但是有两个例外,其中一个就是允许令一个指向常量的指针指向一个非常量对象:

double dval = 3.14;
cptr = &dval;

所谓指向常量的指针常量引用,不过是指针或引用“自以为是”,他们觉得自己指向了常量,所以自觉地不去改变所指对象的值。

const 指针
指针是对象而引用不是,可以像对其他对象类型一样,把指针本身定为常量。常量指针(const pointer) 必须初始化,而且一旦初始化完成,它的值就不能改变了。把 * 放在 const 前面来说明指针是一个常量,说明指针变量本身是不能变的:

int errNumb = 0;
int *const curErr = &errNumb; // curErr will always point to errNumb
const double pi = 3.14159;
const double *const pip = &pi; // pip is a const pointer to a const object

这个例子中 pip 是什么类型呢?

像在指向指针的引用中分析 r 的定义时一样,我们从右往左剖析。
离 pip 最近的是 const,说明 const 是常量对象就,声明符中的下一个符号是 *,意思是 pip 是常量指针,基本数据类型为 double,最左侧 const 说明 pip 是指向常量的指针。所以 pip 就是一个指向常量的常量指针

练习 2.27:下面哪些初始化是合法的?请说明原因。
(a)int i = -1, &r = 0;
(b)int *const p2 = &i2;
(c)const int i = -1, &r = 0;
(d)const int *const p3 = &i2;
(e)const int *p1 = &i2;
(f)const int &const r2;
(g)cosnt int i2 = i, &r = i;

答:
(a)非法的,非常量引用不能引用常量 0。
(b)合法的,p2 是常量指针,指向 i2 后不能指向其他对象。
(c)合法的,i 是常量 -1,r 是常量引用,值为0.
(d)合法的,p3是一个常量指针,p3永远指向 i2,且p3是指向常量的指针,不能通过 p3 修改 i2 的值。
(e)合法的,p1 指向一个常量,不能通过 p1 修改 i2 的值。
(f)非法的,指向常量的指针定义时必须初始化。
(g)合法的,i2是一个常量,r是一个常量引用。

练习 2.28:说明下面这些定义时什么意思,挑出其中不合法的。
(a) int i, *const cp;
(b) int *p1, *const p2;
(c) const int ic, &r = ic;
(d) const int *const p3;
(e) const int *p;

答:
(a) 不合法,常量指针必须初始化。
(b)同上
(c)不合法,常量必须初始化。
(d)不合法,常量指针必须初始化。
(e) 合法。

练习 2.29:假设已有上一个练习中定义的那些变量,则下面的哪些语句是合法的?
(a) i = ic;
(b) p1 = p3;
(c) p1 = &ic;
(d) p3 = &ic;
(e) p2 = p1;
(f) ic = *p3;

答:
(a) 合法,常量 ic 的值付给了 非常量 i。
(b) 非法,p1 是普通指针,能改变所指对象的值,p3 是指向常量的常量指针,其所指的值不能改变。
(c)非法,p1 是普通指针,ic 是常量。
(d)非法,p3 是常量指针,不能被赋值。
(e) p2 是常量指针不能被赋值。
(f) ic是常量,不能被赋值。

顶层 const

指针本身是个对象,也可以指向另外一个对象。指针本身是不是常量以及指针所指的是不是一个常量是两个问题。用名词顶层const(top-level const) 表示指针本身是个常量,而用名词底层const(low-level const) 表示指针指向常量。

更一般的,顶层 const 可以表示任意的对象是常量。底层 const 则与指针和引用等复合类型的基本类型部分有关。指针类型既可以是顶层 const 也可以是 底层 const,这一点和其他类型有明显区别:

int i = 0;
int *const p1 = &i; // we can't change the value of p1; const is top-level
const int ci = 42; // we cannot change ci; const is top-level
const int *p2 = &ci; // we can change p2; const is low-level
const int *const p3 = p2; // right-most const is top-level, left-most is not
const int &r = ci; // const in reference types is always low-level

执行对象的 copy 操作时,常量是顶层 const 还是底层 const 区别明显。其中顶层const不受影响:

i = ci; // ok: copying the value of ci; top-level const in ci is ignored
p2 = p3; // ok: pointed-to type matches; top-level const in p3 is ignored

底层const执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层 const 资格,或者拷出对象的数据类型能转换为拷入对象的数据类型:

int *p = p3; // error: p3 has a low-level const but p doesn't
p2 = p3; // ok: p2 has the same low-level const qualification as p3
p2 = &i; // ok: we can convert int* to const int*
int &r = ci; // error: can't bind an ordinary int& to a const int object
const int &r2 = i; // ok: can bind const int& to plain int

练习 2.30:对于下面的这些语句,请说明对象被声明成了顶层 const 还是底层 const?

const int v2 = 0; int v1 = v2;
int *p1 = &v1, &r1 = v1;
const int *p2 = &v2, *const p3 = &i, &r2 = v2;
答: 顶层 const:v2,p3,r2。 底层 const:p2,r2。

练习 2.31:假设有上一个练习的声明,下列哪些语句是合法的?请说明顶层const 和底层 const 在每个例子中有何体现。

r1 = v2;
p1 = p2; p2 = p1;
p1 = p3; p2 = p3;

答:
第一个合法
第二个不合法,p1 是普通指针,p2 是底层指针,底层指针不能转化为普通指针。
第三个合法,int *能转化成const int*。
第四个不合法,const int* 不能转化成int*。
第五个合法,都是底层指针。

constexpr 和常量表达式

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

一个对象是不是常量表达式由它的数据类型和初始值共同决定:

const int max_files = 20; // max_files is a constant expression
const int limit = max_files + 1; // limit is a constant expression
int staff_size = 27; // staff_size is not a constant expression
const int sz = get_size(); // sz is not a constant expression

constexpr 变量

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

constexpr int mf = 20; // 20 is a constant expression
constexpr int limit = mf + 1; // mf + 1 is a constant expression
constexpr int sz = size(); // ok only if size is a constexpr function

constexpr 函数在编译时就可以确定计算结果,这样就能用 constexpr 函数去初始化 constexpr 变量了。

字面值类型

指针和引用都能定义成 constexpr,但是初始值受到严格限制。constexpr 指针的初始值必须是0、nullptr 或者是存储在某个固定地址中的对象。
函数体内定义的普通变量一般并非存放在固定地址中,因此 constexpr 指针不能指向这样的变量。相反,函数体外定义的变量地址固定不变,可以用来初始化 constexpr 指针。函数体内的静态变量和函数体外的变量一样,也可以用来初始化 constexpr 指针。

指针和 constexpr

在 constexpr 声明中如果定义了一个指针,限定符 constexpr 仅对指针本身有效,与指针所指的对象无关。

const int *p = nullptr;   //p 是一个指向整型常量的指针,底层指针。
constexpr int *q = nullptr;// q 是一个指向整数的常量指针,顶层指针。

constexpr 指针可以指向常量也可以指向非常量。

练习 2.32:下面的代码是否合法?如果非法,请将其修改正确。

int null = 0, *p = null;

答:非法的,null 是 int 变量,p 是一个指针,指针要指向 null 的地址,正确的写法是:

int null = 0, *p = &null;

处理类型

类型别名

类型别名(type alias) 是一个名字,它是某种类型的同义词。

有两种方法可用于定义类型别名。传统的方法是使用 typedef:

typedef double wages;    //wages 是 double 的同义词
typedef wages base, *p;  //base 是 double 的同义词,p 是 double* 的同义词

新方法是使用别名声明(alias declaration) 来定义类型的别名:

using SI = Sales_item;  

指针、常量和类型别名
如果某个类型别名指代的是复合类型或常量,把它用到声明语句中会产生意想不到的结果:

typedef char *pstring;
const pstring cstr = 0; // cstr is a constant pointer to char
const pstring *ps; // ps is a pointer to a constant pointer to char

pstring 是指向 char 的指针,因此,const pstring 就是指向 char 的常量指针。
如果将 pstring 换成 char * 对 cstr 解读会产生错误的结果:

const char *cstr = 0;

cstr 成了指向 const char 的指针。与原来的声明截然不同。

auto类型说明符

C++11新标准引入了 auto 类型说明符,能让编译器自动分析表达式所属的类型。auto 定义的变量必须有初始值。

// 由val1和val2相加的结果可以推断出item的类型
auto item = val1 + val2;    // item初始化为val1和val2相加的结果

使用 auto 也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:

auto i = 0, *p = &i; // ok: i is int and p is a pointer to int
auto sz = 0, pi = 3.14; // error: inconsistent types for sz and pi

复合类型、常量和auto

编译器推断出来的 auto 类型有时和初始值的类型并不完全一样,编译器会适当地改变结果类型,使其更符合初始化规则。

  1. 当引用被用作初始值时,真正参与初始化的其实是引用对象的值。编译器以引用对象的类型作为 auto 的类型。
int i = 0, &r = i;
auto a = r;     // a是一个整数(r是i的别名,而i是一个整数)

  1. auto 一般会忽略顶层 const,同时底层 const 则会保留下来。
const int ci = i, &cr = ci;
auto b = ci; // b is an int (top-level const in ci is dropped)
auto c = cr; // c is an int (cr is an alias for ci whose const is top-level)
auto d = &i; // d is an int*(& of an int object is int*)
auto e = &ci; // e is const int*(& of a const object is low-level const)

如果希望推断出的 auto 是一个顶层 const,需要明确指出:

const auto f = ci;    

还可以将引用的类型设为 auto,原来的初始化规则仍然适用:

auto &g = ci;        //g 是一个整型常量引用。
auto &h = 42;		 //不能为非常量引用绑定字面值
const auto &j = 42;  //正确:可以为常量引用绑定字面值。

要在一条语句中定义多个变量,切记, 符号&和*只从属于某个声明符,而非基本数据类型的一部分, 因此初始值必须是同一种类型:

auto k = ci, &l = i;      // k是整数,l是整型引用
auto &m = ci, *p = &ci;   // m是对整型常量的引用,p是指向整型常量的指针
// 错误:i的类型是int而&ci的类型是const int
auto &n = i,*p2 = &ci;

练习 2.33:利用本节定义的变量,判断下列语句的运行结果。
a = 42; b = 42; c = 42;
d = 42; e = 42; g = 42;

答:
abc都是整数,正确;de是指针,错误;g是整型常量引用,初始化后不能对其赋值,错误。

练习 2.34:基于上一个练习中的变量和语句编写一段程序,输出赋值前后变量的内容,你刚才的推断正确吗?

答:正确

练习 2.35:判断下列定义推断出的类型是什么,然后编写程序进行验证。

const int i = 42;
auto j = i; const auto &k = i; auto *p = &i;
const auto j2 = i, &k2 = i;

答:
j是整型 int,拷贝时候忽略 i 的顶层const;
k是整型常量引用;
p是整型指针;
j2是整型常量;
k2是整型常量引用。

decltype 类型指示符

C++11新增 decltype 类型指示符,作用是选择并返回操作数的数据类型,此过程中编译器不实际计算表达式的值。

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

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&, y绑定到变量x
decltype(cj) z;     	// error: z是一个引用,必须初始化

decltype 和引用

如果 decltype 使用的表达式不是一个变量,则 decltype 返回表达式结果对应的类型;如果表达式的内容是解引用操作,则 decltype 将得到引用类型:

// decltype的结果可以是引用类型
int i = 42, *p = &i , &r = i;
decltype (r + 0) b; // 正确:加法的结果是int,因此b是一个(未初始化的)int
decltype (*p) c; 	// 错误:c是int&,必须初始化

decltype 和 auto 的另一处重要区别是,decltype 的结果类型与表达式形式密切相关。注意:如果 decltype 使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,则 decltype 会得到引用类型,因为变量是一种可以作为赋值语句左值的特殊表达式。

// decltype的表达式如果是加上了括号的变量,结果将是引用
decltype ((i)) d;  // 错误:d是int&,必须初始化
decltype (i) e;    // 正确:e是一个(未初始化的)int

练习 2.36:关于下面的代码,请指出每一个变量的类型以及程序结束时他们各自的值。
int a = 3, b = 4;
decltype(a) c = a;
decltype((b)) d = a;
++c;
++d;

答:a是整数,值为4;b是整数,值为4;c是整数,值为4;d是整型引用,值为4.

练习 2.37:赋值是会产生引用的一类经典表达式,引用的类型就是左值的类型。也就是说,如果 i 是 int,则表达式 i=x 的类型是 int&。根据这一特点,请指出下面代码中每一个变量的类型和值。

int a = 3, b = 4;
decltype(a) c = a;
decltype(a = b) d = a;

答:c 是整型,值为 3;a=b 作为decltype 的参数,只给 decltype 提供类型,并不进行赋值,所以 a 的值不变,还是 3;d 是 a 的引用,值为 3;b为整数3。

Exercise 2.38: Explain the difference between types specified by decltype and types specified by auto. Please give an example, the type specified by decltype is the same as the type specified by auto; give another example, the type specified by decltype is different from the type specified by auto.

Answer: There are three main differences between auto and decltype:

First, the auto type specifier uses the compiler to calculate the initial value of the variable to infer its type, while decltype also lets the compiler analyze the expression and get its type, but it does not actually calculate the value of the expression.

Second, sometimes the auto type inferred by the compiler is not exactly the same as the type of the initial value, and the compiler will appropriately change the result type to make it more consistent with the initialization rules. For example, auto generally ignores the top-level const and keeps the bottom-level const. In contrast, decltype preserves the variable's top-level const.

Third, unlike auto, the result type of decltype is closely related to the expression form. If a pair of parentheses are added to the variable name, the obtained type will be different from that without parentheses. If decltype uses a variable without parentheses, the result is the type of the variable; if one or more parentheses are added to the variable, the compiler will infer the reference type.

Examples are as follows:

#include <iostream>
using namespace std;
#include<typeinfo>
int main()
{
    
    
	int a = 3;
	auto c1 = a;
	decltype(a)c2 = a;
	decltype((a))c3 = a;

	const int d = 5;
	auto f1 = d;
	decltype(d)f2 = d;

	cout << typeid(c1).name() << endl;
	cout << typeid(c2).name() << endl;
	cout << typeid(c3).name() << endl;
	cout << typeid(f1).name() << endl;
	cout << typeid(f2).name() << endl;

	c1++;
	c2++;
	c3++;
	f1++;
	f2++; // 错误:f2是整型常量,不能执行自增操作
	cout << a << " " << c1 << " " << c2 << " " << c3 << " " << f1 << " " << f2 << endl;
	return 0;
}

For the first group of type inference, a is a non-constant integer, the inferred result of c1 is an integer, the inferred result of c2 is also an integer, and the inferred result of c3 is an integer reference because of the extra pair of parentheses added to the variable a. c1, c2, and c3 perform auto-increment operations in sequence, because c3 is an alias of variable a, so c3 auto-increment is equivalent to a auto-increment, and finally the values ​​of a, c1, c2, and c3 all become 4.

For the second group of type inference, d is a constant integer containing top-level const. Using auto to infer the type automatically ignores the top-level const, so the inference result of f1 is an integer; decltype retains the top-level const, so the inference result of f2 is an integer constant . The self-increment operation of f1 can be performed normally, but the value of the constant f2 cannot be changed, so it cannot be self-incremented.

custom data structure

The content of the class will be introduced in detail later.



Follow Bozai and don't get lost

Guess you like

Origin blog.csdn.net/weixin_45773137/article/details/125689394