《C++ Primer》第Ⅰ部分

第1章 开始

第Ⅰ部分 C++基础

第2章 变量和基本类型

使用int执行整数运算(而非short);使用doule执行浮点数运算(而非float)。

在算术表达式中不要使用char或bool。

当明确知晓数值不可能为负时,选用无符号类型;切勿混用带符号类型和无符号类型。

避免无法预知和依赖于实现环境的行为(如将int的尺寸视为定值将导致程序不可移植)。

变量提供一个具名的、可供程序操作的存储空间。

数据类型决定着变量所占内存的大小和布局方式、该空间能存储的值的范围以及变量能参与的运算。

当变量在创建时获得了一个特定的值,称为初始化。初始化与赋值不同,初始化是创建变量时赋予其一个初始值,赋值则是用新值来代替变量的当前值。

使用“{}”可进行列表初始化,列表初始化禁止缩窄变换。

如果定义变量时没有指定初值,变量将被默认初始化。内置类型的变量若未被显式初始化,且定义于任何函数体之外,其被初始化为0;若在函数体内部,则不被初始化,即含有一个不确定的值。类对象的初始化则由类确定。

应尽量初始化每一个内置类型的变量。

声明使得名字为程序所知,定义负责创建与名字关联的实体。二者都规定了变量的类型和名字,但定义除此之外还申请了存储空间,也可能为变量赋一个初始值。

如果想声明一个变量而非定义,需要在变量名前添加关键字extern且不显式初始化变量。任何包含了显示初始化的声明即称为定义(显示初始化会抵消extern的作用)。

变量能且仅能被定义一次,但是可以被多次声明。这就要求在多个文件使用同一变量时将声明和定义分离。定义只能出现在一个文件中,其他使用该变量的文件需对其进行声明(不可再次定义)。

在变量第一次被使用的地方附近对其进行定义是一种好的选择,且有助于为其赋一个较为合理的初值。

引用并非对象,而是一个已经存在的对象的别名,因此也无法定义引用的引用;引用的初值必须为一个对象而不可是字面值或表达式的计算结果。

与常规变量的初始化不同,引用的定义是将引用和其初值绑定在一起,而非将初值拷贝给引用。引用必须初始化,且一旦初始化完成,引用将无法重新绑定到另一个对象。

指针本身是一个对象(与引用不同),因此可以对指针进行赋值和拷贝,且指针可在生命周期内指向不同的对象。

空指针不指向任何对象,可以通过字面值nullptr(C++11)或字面值0来初始化空指针。

指针虽然无需在定义时赋初值(与引用不同),但使用未初始化的指针式引发运行时错误的一大原因,因此,尽可能的初始化所有指针。

赋值永远改变的是等号左侧的对象。

void*指针(与空指针不同)是一种特殊的指针类型,可用于存放任意类型对象的地址。但void*指针仅可与其他指针相比较、作为函数的输入或输出、赋值给另外一个void*指针,而不可以直接操作void*指针所指的对象(因为不明确void*指针所指对象的类型,也就无法知晓可对其进行的操作)。

一条声明语句由一个基本数据类型(int、double等)和一组声明符(单个或由逗号分开的多个变量名称)组成,&(引用)、*(指针)等类型修饰符可视作声明符的一部分。

用于声明引用和指针的类型修饰符&与*仅仅修饰其后一个变量(语句int* p1, p2;仅将p1声明为指针,可将&或*与变量名连在一起写,以强调这一点)。

对于比较复杂的指针或引用声明语句,可从右向左阅读,离变量名最近的符号对变量的类型有最直接的影响。

使用const限定符可定义常量。由于常量一旦创建后值就不再改变,因此const对象必须初始化,且用于初始化const对象的另外一个对象是否为const均可。

在默认状态下,const对象仅在文件内有效。若想实现在不同文件中使用同一个const对象,则无论是声明或定义,均需添加extern关键字(亦即在多个文件之间共享const对象,则必须在其定义前添加extern关键字)。

可声明对常量的引用(例如:const int &r = i;),使得该引用不能被用作修改其指向的对象;从另一个角度来看,试图让非常量引用指向一个常量对象,是错误的。

允许为一个对常量的引用绑定非常量的对象、字面值甚至是一个表达式。

可以为一个对常量的引用指定字面值,不可以为一个对非常量的引用指定字面值。

对常量的引用可以指向非常量对象、字面值、甚至是一个表达式。总的来说,对常量的引用可以指向常量对象或非常量对象,但常量对象必须由对常量的引用来指向。

对常量的引用仅对引用可参与的操作做出了限定,对于其指向的对象本身(若非const对象),仍可通过其他途径改变其值。

指向常量的指针(例如:const double *p = &d; )不能用于改变其所指对象,因此对于常量对象的地址,只能使用这种指向常量的指针。但执行常量的指针运行指向一个非常量对象,因为和对常量的引用一样,仅仅是不能通过该指向常量的指针改变对象的值,而这个值允许通过其他途径改变。

指针本身既是对象(与引用不同),因此允许将指针声明为常量,即为常量指针(例如:int *const p = i;)。常量指针作为常量对象(与其他常量对象相同,一旦定义不能改变),必须初始化(即在定义时赋予初始值)。

对常量的引用、常量引用、指向常量的指针、常量指针这五者之间的联系与区别:

  • 对常量的引用
    • 定义方法:const int &r = i;
    • 初始化:定义时初始化(无论是对常量还是非常量的引用都必须在定义时初始化)
    • 可引用的对象:常量、非常量
    • 自身是否可以修改:不可修改(引用一旦定义则无法重新绑定到另外一个对象)
    • 是否可以修改引用对象的值:不可通过对常量的引用修改其引用的对象的值,但其引用的对象若为非常量,则可通过其他途径修改
  • 常量引用
    • 无“常量引用”这一说法,因为引用在整个声明周期内仅能绑定同一个对象,因此从某种意义上讲所有的引用均为常量
  • 指向常量的指针(底层const,指针指向的对象是常量)
    • 定义方法:const double *p = &d
    • 初始化:无需定义时初始化(可先定义后赋值)
    • 可指向的对象:常量、非常量
    • 自身是否可以修改:可以修改
    • 是否可以修改指向对象的值:不可通过指向常量的指针修改其指向的对象的值,但其指向的对象若为非常量,则可通过其他途径修改
  • 常量指针(顶层const,指针本身是常量)
    • 定义方法:double *const p = &d; // 将const紧邻p,说明p本身是一个常量,*放在cosnt之前,表示此常量是一个指针(不变的是指针而非指针所指向的值)
    • 初始化:定义时初始化(常量指针作为常量对象的一种,与其他常量相同,必须定义时初始化)
    • 可指向的对象:常量、非常量
    • 自身是否可以修改:不可修改(常量指针与其他常量相同,一旦定义不可修改)
    • 是否可以修改指向对象的值:若指向常量对象则不可修改;若指向非常量对象则可修改

*注:从右向左阅读,从与对象最近的符号开始分析并依次向左扩展,从而确定声明的真正含义

类型别名可以让复杂的类型名字变得简单明了、易于理解和使用,定义类型别名的方法有:

  • 使用关键字typedef

关键字typedef是作为声明语句中的基本数据类型的一部分出现(一条声明语句由一个基本数据类型和紧随其后的一个声明符列表组成),含有typedef的声明语句定义的不再是变量而是类型别名。和普通声明语句一样,这里的声明符也可以包含类型修饰,从而也能由基本数据类型构造出复合类型来。

typedef double wages; // 使用wages代表double
typedef wages base, *p; // 使用base代表double,使用p代表double*(指向double的指针),其中base与*p为声明符列表

需要注意的是,不可以将类型别名替换成它原来的样子来理解使用了类型别名的声明语句(不同于通过#define定义的宏,可以直接替换),例如:

typedef char *pstring;
const pstring cstr = 0; // cstr是指向char的常量指针
const pstring *ps; // ps是一个指针,其指向一个指针,这个指针是“指向char的常量指针

不可以使用char *来代替pstring使const pstring cstr = 0成为const char *cstr = 0。上述两条声明语句的基本数据类型都是const pstring,const是对给定类型的修饰,pstring是指向char的指针,因此cosnt pstring是指向char的常量指针,而非指向常量字符的指针(要正确理解使用类型别名的声明语句,应该用类型别名原来的的含义而非原来的形式来代替类型别名)。

  • 使用using别名声明  
using SI = Sales_item; // 用SI代表Sales_item

使用auto类型说明符可以让编译器分析表达式所属的类型,但auto定义的变量必须由初始值,且一条auto语句声明的多个变量必须为同一类型。

编译器推断出的auto类型在有些情况下与初始值类型并不完全一致,如将引用所指对象的类型作为auto类型而非引用类型,如对顶层cosnt的忽略及对底层cosnt的保留等(若希望推断出的auto类型是顶层const则需要显式指出)。

可以将引用的类型设为auto(如:auto &r = i,但对于指针无需使用*,auto p = &i即可表示p是指向i的指针)。

使用#define可将一个名字定义为预处理变量,#ifdef当且仅当变量已经定义时为真,#ifndef当且仅当变量未定义时为真,一旦结果为真,执行后续操作直至遇到#endif为之。

预处理变量无视C++语言中关于作用域的规则。

第3章 字符串、向量和数组

string对象的初始化方式:

string s1; // 将s1默认初始化为空串
string s2(s1); // 使用s1为s2进行初始化,直接初始化
string s3("string 3"); // 使用字面值为s3进行初始化,直接初始化
string s4 = "string 4"; // 使用字面值为s4进行初始化,拷贝初始化
string s5(6, '6'); // 将s5初始化为6个6

string通过括号进行初始化为直接初始化,通过等号进行初始化为拷贝初始化。

通过cin读取string对象(如cin >> s),则string对象会忽略开头处的空白(空格符、换行符、制表符等),从第一个真正的字符读取,直到遇见下一个空白。也就是说,当通过cin读取由空格分开的多个单词组成的句子时,只会将第一个单词读到string对象中。

多个输入输出可连续使用(如cin >> s1 >> s2; cout << s1 << s2;),但对空白的忽略规则不变。

通过getline(cin, s);可以读取输入流中的内容到s中,直到遇到换行符。s中将包含输入流开头处的空白、字符中间的空白以及字符末尾的空白,但读入换行符会被丢弃。

getline()的返回值可作为while等循环的判断条件,如:while (getline(cin, s)) { },即将getline返回的istream类对象作为循环判断条件。当流中遇到文件结束符或无效输入(如输入与目标类型不匹配),则getline返回fales。从键盘输入文件结束符的方法如下:

Windows系统 键入Ctrl+Z后按Enter
Linux系统 Ctrl+D

string类的empty成员函数可用于判断对象是否为空并返回相应的布尔值(空白(如多个空格)不为空,而开头即为换行符则为空)。

string类的size成员函数可用于返回string类对象的长度(包括开头、中间与末尾的空白)。size的返回值实际上为string::size_type,其为无符号类型,因此不要在有size函数的表达式中再使用int,以防止有符号无符号类型混用引发问题。

string类还可使用比较运算符进行比较、赋值运算符进行赋值以及加法运算符进行相加。但进行string相加时,加号两侧的对象至少有一个为string而不可均为字符串字面值(即不可将两个字符串字面值使用加法运算符相加)。虽然可用字符串字面值为string赋值,但string与字符串字面值并非相同的类型。

若想对于string对象中的每个字符进行操作,可以使用范围for语句,范围for语句将遍历给定序列中的每个元素并对序列中的每个值执行某种操作:

for (declaration : expression)
    statement

其中,expression为一个对象,用于表示一个序列,declaration表示一个变量,该变量被用于访问序列中的基础元素。每次迭代,declaration部分的变量会被初始化为expression部分的下一个元素值。对于string使用范围for语句的一个例子是:

string s("some string");

for (auto c : s)
    cout << c << endl;

string对象可以使用下标运算符[]对其中的字符进行索引,下标从0记起。

vector是一个类模板(C++中既有类模板,又有函数模板),表示对象的合集,其中所有对象的类型都相同,且每个对象都有一个与之对应的索引,索引用于访问对象。模板本身不是类,可视为编译器生成类或函数的一份说明。当使用vector类模板时,需要指出编译器应将类实例化成何种类型,此类型通过在vector后的尖括号中指出,如vector<int> i;指出i为容纳int类型的vector容器。

与内置数组类似,vector中元素也可通过[]进行索引。但与数组不同的是,vector对象可通过push_back方法向对象尾端添加元素,如v.push_back(t)会将t添加到v的尾端,但数组的大小确定且不能随意向数组中增加元素。

对于string或vector对象元素,除使用[]进行索引之外,还可以通过迭代器实现同样目的。迭代器与指针类似,但其可以访问vector等多种容器中的元素。可以通过容器的begin成员获得指向容器中第一个元素的迭代器,并通过end成员获得指向容器中最后一个元素的迭代器。迭代器的基本运算如下:

auto b_iter = v.begin() 获得指向容器中第一个元素的迭代器b_iter
auto e_iter = v.end() 获得指向容器中最后一个元素后面的迭代器e_iter
*iter 获得迭代器iter所指元素的引用
iter->mem 获取迭代器所指向元素的mem成员,等价于(*iter).mem
++iter 令迭代器iter指向容器中的下一个元素
--iter 零迭代器iter指向容器中的上一个元素
iter1 == iter2 判断两个迭代器iter1和iter2是否相等
iter1 != iter2 判断两个迭代器iter1和iter2是否不相等

 

第4章 表达式

第5章 语句

第6章 函数

第7章 类

类的成员函数通过一个名为this的额外隐式参数来访问调用它的那个对象,当调用一个成员函数时,则用请求该函数的对象地址初始化this。

常量成员函数,亦即成员函数的参数列表之后有一个cosnt关键字,实际上表示该成员的隐式形参this指针是指向常量的指针,所以常量成员函数也不能改变调用它的对象的内容。

7.5

对于类的构造函数,使用构造函数初始值列表语法与在构造函数体中通过赋值进行初始化存在如下区别,即如果没有在构造函数的初始值列表中显示的初始化成员,则成员将在构造函数体之前执行默认初始化,对于一般的数据成员,通过列表初始化与赋值的区别不大,但对于const或者引用成员,则必须对其初始化,亦即使用函数初始值列表,随着构造函数体一开始执行,初始化就完成了,这也是对cosnt或者引用类型成员的唯一机会。

需要注意的一点是,构造函数的初始值列表只用于初始化成员的值,而不限定初始化的具体执行顺序,因此尽量避免使用某些成员初始化其他成员。

posted @ 2021-02-03 12:31  溪嘉嘉  阅读(91)  评论(0编辑  收藏  举报