C++ Primer(读书笔记)

C++源文件通常以.cc、.cxx、.cpp、.cp、.C作为后缀来命名

C++语言未定义输入输出语句,而是提供了一个全面的标准库来提供IO机制,对应 iostream、fstream、sstream

std::cout<<"Enter Two Numbers"<<std::endl;。这条语句执行了一个表达式,在C++中,一个表达式产生一个计算结果,它由一个或多个运算对象和一个运算符组成,其中,::为作用域运算符,std代表命名空间

C++注释:双斜线注释//,界定符注释/* */

控制流语句:while语句、for语句、if语句,在Unix系统中,Ctrl+D代表文件结束符

一般而言,C++类的作者决定了类类型对象上可以使用的所有操作

item1.isbn(),其中涉及到点运算符、调用运算符

术语:公共语法特征的实现细节

C++内置类型:整型、浮点型、布尔型、字符型、空类型

带符号类型和无符号类型:除去布尔型和扩展的字符型之外,其他整型可以划分为带符号的和无符号的两种。类型int、short、long和long long都是带符号的,通过在这些类型名前添加unsigned就可以得到无符号类型,例如unsigned long。类型unsigned int 可以缩写为 unsigned。与其他整型不同,字符型被分为了三种,char、signed char、unsigned char。类型char实际上会表现为上述两种形式中的一种,具体是哪种由编译器决定。C++标准并没有规定带符号类型应如何表示,但是约定了在表示范围内正值和负值的量应该平衡,因此,8比特的signed char实际表示范围为-128至127

类型转换:对象的类型定义了对象能包含的数据和能参与的运算,其中一种运算被大多数类型支持,就是将对象从一种给定的类型转换为另一种相关类型

C++支持有损精度的类型转换,这方面Java会更严格一些,需要强转

在C++中,我们要尽量避免依赖于实现环境的行为,如果我们把int的尺寸看成是一个确定不变的已知值,那么这样的程序是不可移植的

含有无符号类型的表达式

不要给无符号对象赋一个负值,但是有一些隐含场景需要注意

  • 无符号数和带符号的负值相加,带符号的负值会转换成无符号数
  • 从无符号数中减去一个值时,需要确保结果不能是一个负值,特别是用在循环判断的场景
  • Java中不存在无符号类型

字面值常量

每个字面值常量都对应一种数据类型,字面值常量的形式和值决定了它的数据类型

整型字面值:以0开头的整数代表八进制数,以0x或0X开头的代表十六进制,对应20、024、0x14,默认情况下,十进制字面值是带符号值,八进制和十六进制字面值即可能是带符号的也可能是无符号的。十进制字面值的类型是int、long、long long中尺寸最小的那个,前提是这种类型能容纳下当前的值。八进制和十六进制字面值的类型是能容纳其数值的int、unsigned int、long、unsigned long、long long、unsigned long long中的尺寸最小者。注意,类型short没有对应的字面值

浮点型字面值是一个double,字面值表现为一个小数或以科学计数法表示的指数,其中指数部分用E或e标识:3.14、3.14E0、0.、0e0、.001

字符和字符串字面值

由单引号括起来的一个字符称为char型字面值,双引号括起来的零个或多个字符构成字符串型字面值。字符串字面值的类型实际上是由常量字符构成的数组,编译器在每个字符串的结尾处添加一个空字符(‘\0'),因此字符串字面值的实际长度要比它的内容多1。如果两个字符串字面值位置紧邻且由空格、缩紧和换行符分隔,则他们实际上是一个整体

转义序列

有两类字符程序员不能直接使用,需要转义:一类是不可打印的字符,一类是C++语言中有特殊含义的字符(单引号、双引号、问号、反斜线)。我们也可以使用泛化的转义序列,其形式是\x后紧跟1个或多个十六进制数字,或者\后紧跟1个、2个或3个八进制数字

指定字面值的类型

布尔字面值和指针字面值

true和false是布尔值的字面值;nullptr是指针字面值

变量定义:类型说明符+变量声明符

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

初始化类型:直接初始化、列表初始化、拷贝初始化、值初始化、默认初始化

变量初始化的4种方式:

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

其中,用花括号来初始化变量的方式称为列表初始化,当用于内置类型的变量时,如果初始值存在丢失信息的风险,则编译器将报错

默认初始化

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

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

变量声明和定义的关系

为了允许把程序拆分成多个逻辑部分来编写,C++语言支持分离式编译机制,为了支持分离式编译,C++语言将声明和定义区分开来。声明使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义负责创建与名字关联的实体

变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显示地初始化变量

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

标识符

C++的标识符由字母、数字和下划线组成,其中必须以字母或下划线开头。标识符的长度没有限制,但是对大小写敏感。C++语言保留了一些名字供语言本身使用,这些名字不能被用作标识符。同时,C++也为标准库保留了一些名字。用户自定义的标识符不能连续出现两个下划线,也不能以下划线紧连大写字母开头。此外,定义在函数体外的标识符不能以下划线开头

作用域

作用域是程序的一部分,C++语言中大多数作用域都以花括号分隔

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

嵌套作用域

作用域能彼此包含,被包含的作用域称为内层作用域,包含着别的作用域的作用域称为外层作用域

作用域中一旦声明了某个名字,它所嵌套着的所有作用域中都能访问该名字(内部类可以访问外部类的私有成员,但外部类无法访问内部类的私有成员)。同时,允许在内层作用域中重新定义外层作用域已有的名字。我们也可以显示的访问外层的变量,例如::reused

复合类型

引用(左值引用):引用为对象起了另外一个名字,通过将声明符写成&d的形式来定义引用类型,其中d是声明的变量名。一般在初始化变量时,初始值会被拷贝到新建的对象中,然而定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化

引用并非对象,它只是为一个已经存在的对象所起的另外一个名字

所有引用的类型都要和与之绑定的对象严格匹配,但有两种情况除外,我们可以将const引用绑定到非const对象,也可以将基类引用绑定到派生类对象

除了const引用外,引用不能与字面值或某个表达式的计算结果绑定在一起

指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象

指针无须在定义时赋值,和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值

所有指针的类型都要和它指向的对象严格匹配,但有两种情况除外,我们可以将const指针指向非const对象,也可以将基类指针指向派生类对象

解引用操作仅适用于那些确实指向了某个对象的有效指针

空指针:nullptr、0、NULL。其中,NULL为预处理变量,当用到一个预处理变量时,预处理器会自动地将它替换为实际值,因此用NULL初始化指针和用0初始化指针是一样的,但在新标准下,最好使用nullptr

任何非0指针对应的条件值都是true。如果两个指针存放的地址值相同,则它们相等

void*是一种特殊的指针类型,可用于存放任意对象的地址,但是我们对该地址中到底是个什么类型的对象并不了解

指向指针的指针:int **,指向指针的引用:int *&

const对象:const对象一旦创建后就不能再改变,所以const对象必须初始化

对象的类型决定了其上的操作,只能在const类型的对象上执行不改变其内容的操作

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

当以编译时初始化的方式定义一个const对象时(const int bufSize = 512;),编译器将在编译过程中把用到该变量的地方都替换成对应的值。为了执行上述替换,编译器必须知道变量的初始值。如果程序包含多个文件,则每个用了const对象的文件都必须得能访问到它的初始值才行。要做到这一点,就必须在每一个用到变量的文件中都有对它的定义。为了支持这一用法,同时避免对同一个变量的重复定义,默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量

某些时候有这样一种const变量,它的初始值不是一个常量表达式,但又确实有必要在文件间共享。解决办法是,对于const变量不管是声明还是定义都添加extern关键字,这样只需定义一次就可以了

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

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

此处 ri 引用了一个int型的数。对ri的操作应该是整数运算,但 dval 却是一个双精度浮点数而非整数。因此为了确保让 ri 绑定一个整数,编译器把上述代码变成了如下形式:const int temp = dval; const int &ri = temp; 在这种情况下,ri 绑定了一个临时量对象。当 ri 不是常量时,我们可以通过 ri 来改变 dval 的值,因此,编译器无法将引用绑定到临时量上,C++语言也就把这种行为归为非法

对const的引用可能引用一个并非const的对象,必须认识到,常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未做限定

指向常量的指针:和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量,const double *cptr;

常量指针:指针是对象而不是引用,允许把指针本身定为常量。常量指针必须初始化,而且一旦初始化完成,它的值就不能再改变了,const int *const curErr = &errNumb;

顶层 const

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

当执行对象的拷贝操作时,常量是顶层const还是底层const区别明显,其中,顶层const不受什么影响,但是底层const的限制却不能忽略,当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之则不行

常量表达式:指值不会改变并且在编译过程就能得到计算结果的表达式。显然,字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式,一个对象或表达式是不是常量表达式由它的数据类型和初始值共同决定:

  • const int max_files = 20;            // max_files是常量表达式
  • const int limit = max_files + 1;   // limit是常量表达式
  • int staff_size = 27;                     // staff_size不是常量表达式
  • const int sz = get_size();           // sz不是常量表达式

constexpr变量

将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。新标准允许定义一种特殊的constexpr函数,这种函数应该足够简单以使得编译时就可以计算其结果,这样就能用constexpr函数去初始化constexpr变量了

字面值类型

常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为“字面值类型”。其中,算数类型、引用和指针都属于字面值类型。自定义类、IO库、string类型则不属于字面值类型,也就不能被定义成constexpr

尽管指针和引用都能定义成constexpr,但它们的初始值却受到严格限制。一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象(所有定义于函数体之外的对象、函数体内的局部静态对象)

指针和constexpr

限定符constexpr仅对指针有效,与指针所指的对象无关:

  • const int *p = nullptr;            // p是一个指向整型常量的指针
  • constexpr int *q = nullptr;     // q是一个指向整型的常量指针

关键在于constexpr把它所定义的对象置为了顶层const:cosntexpr const int *p = &i

类型别名

  • typedef double wages;         // wages 是 double 的同义词
  • typedef wages base, *p;      // base 是 double 的同义词,p 是 double* 的同义词
  • const p cp = 0;                   // cp 是指向 double 的常量指针

新标准规定了一种新的方法,使用别名声明来定义类型的别名:

using SI = Sales_item;        // SI 是 Sales_item 的同义词

auto类型说明符

auto 让编译器通过初始值来推算变量的类型,显然,auto定义的变量必须有初始值。使用auto也能在一条语句中声明多个变量,因为一条声明语句只能有一个基本数据类型,所有该语句中所有变量的初始基本数据类型都必须一样

当引用被用作初始值时,真正参与初始化的其实是引用对象的值。此时编译器以引用对象的类型作为auto的类型

auto一般会忽略掉顶层const,同时底层const则会保留下来,如果希望推断出的auto类型是一个顶层const,需要明确指出

const auto f = ci;        const auto &j = 42;

decltype类型指示符

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

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

decltype处理顶层const和引用的方式与auto不同,如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内)

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

切记:decltype((variable))(注意是双括号)的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用

类数据成员:可以为数据成员提供一个类内初始值。创建对象时,类内初始值将用于初始化数据成员。没有初始值的成员将被默认初始化类内初始值或者放在花括号里,或者放在等号右边,记住不能使用圆括号

编写自己的头文件:类一般都不定义在函数体内,当在函数体外部定义类时,在各个指定的源文件中可能只有一处该类的定义。而且,如果要在不同文件中使用同一个类,类的定义就必须保持一致。为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样

头文件中通常包含那些只能被定义一次的实体,如类、const和constexpr变量等

预处理器概述

确保头文件多次包含仍能安全工作的常用技术是预处理器,它由C++语言从C语言继承而来。预处理器是在编译之前执行的一段程序,可以部分地改变我们所写的程序。之前已经用到的一项预处理功能是#include,当预处理器看到#include标记时就会用指定的头文件的内容代替#include

C++程序还会用到的一项预处理功能是头文件保护符,头文件保护符依赖于预处理变量。预处理变量有两种状态:已定义和未定义。#define指令把一个名字设定为预处理变量,#ifdef和#ifndef分别检查某个指定的预处理变量是否已经定义。一旦检查结果为真,则执行后续操作直到遇到#endif指令为止。预处理变量无视C++语言中关于作用域的规则

using声明:using namespace::name;一旦声明了上述语句,就可以直接访问命名空间中的名字。每个名字都需要独立的using声明。头文件不应包含using声明

直接初始化&拷贝初始化

如果使用等号初始化一个变量,实际上执行的是拷贝初始化,编译器把等号右侧的初始值拷贝到新创建的对象中去。与之相反,如果不使用等号,则执行的是直接初始化

  • string s5 = “hiya";               // 拷贝初始化
  • string s6("hiya");                 // 直接初始化
  • string s7(10, 'c');                 // 直接初始化
  • string s8 = string(10, 'c');    // 拷贝初始化

对于string类的size函数来说,返回一个int或者unsigned似乎都是合理的,但其实size函数返回的是一个string::size_type类型的值。在具体使用的时候,通过作用域操作符来表明名字size_type是在类string中定义的。尽管我们不太清楚string::size_type类型的细节,但有一点是肯定的:它是一个无符号类型的值。如果一条表达式中已经有了size()函数就不要再使用int了,这样可以避免混用int和unsigned可能带来的问题

当把string对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符的两侧的运算对象至少有一个是string

  • string s7 = ("hello" + ", ") + s2;        // 错误:不能把字面值直接相加

使用C++版本的C标准库头文件:cctype和ctype.h的内容是一样的,只不过从命名规范上更符合C++语言的要求。特别的,在名为cname的头文件中定义的名字从属于命名空间std,而定义在名为.h的头文件中的则不然

范围for语句

string str("some string");
for (auto c : str)
  cout << c < endl;

使用范围for语句改变字符串中的字符

string str("some string");
for (auto &c : str)
  c = toupper(c);
cout << s < endl;

下标运算符接收的参数是string::size_type类型的值,这个参数表示要访问的字符的位置:返回值是该位置上字符的引用

下标必须大于0而小于s.size(),超出此范围将引发不可预知的结果

C++语言既有类模版,也有函数模版,其中vector是一个类模版。编译器根据模版创建类或函数的过程称为实例化,当使用模版时,需要指出编译器应把类或函数实例化成何种类型

vector是模版而非类型,由vector生成的类型必须包含vector中元素的类型,如vector<int>,vector<vector<string>>

vector能容纳绝大多数类型的对象作为其元素,但是因为引用不是对象,所以不存在包含引用的vector。除此之外,其他大多数内置类型和类类型都可以构成vector对象,甚至组成vector的元素也可以是vector

C++语言提供了几种不同的初始化方式,在大多数情况下这些初始化方式可以相互等价地使用,但存在一些例外情况:

  • 使用拷贝初始化时,只能提供一个初始值;
  • 如果提供的是一个类内初始值,则只能使用拷贝初始化或使用花括号的形式初始化
  • 如果提供的是初始元素值的列表,则只能把初始值都放在花括号里进行列表初始化,而不能放在圆括号里
    • vector<string> v2("a", "b");        // 错误

值初始化

通常情况下,可以只提供vector对象容纳的元素数量而略去初始值。此时库会创建一个值初始化的元素初值,并把它赋给容器中的所有元素。这个初值由vector对象中元素的类型决定。如果vector对象的元素是内置类型,比如int,则元素初始值自动设为0。如果元素是某种类类型,比如string,则元素由类默认初始化

  • 有些类要求必须明确提供初始值,如果vector对象中元素的类型不支持默认初始化,我们就必须提供初始的元素值。对这种类型的对象来说,只提供元素的数量而不设定初始值无法完成初始化工作
  • 如果只提供了元素的数量而没有设定初始值,只能使用直接初始化
    • vector<int> vi = 10;  //错误:必须使用直接初始化的形式指定向量大小

列表初始值还是元素数量

  • 圆括号提供的值用来构造(construct)vector对象
  • 花括号用来列表初始化vector对象,初始化过程会尽可能地把花括号内的值当成元素初始值的列表来处理,只有无法执行列表初始化时才会考虑其他初始化方式
    • vector<int> v1(10);        // v1有10个元素,每个的值都是0
    • vector<int> v2{10};        // v2有1个元素,该元素的值是10
    • vector<int> v3(10, 1);    // v3有10个元素,每个元素的值都是1
    • vector<int> v4{10, 1};    // v4有两个元素,值分别是10和1
    • vector<string> v5{"hi"};  // 列表初始化,v5有一个元素
    • vector<string> v6("hi");  // 错误:不能使用字符串字面值构建vector对象
    • vector<string> v7{10};    // v7有10个默认初始化的元素
    • vector<string> v8{10, "hi"};   // v8有10个值为“hi”的元素

对vector对象来说,直接初始化的方式适用于三种情况:初始值已知且数量较少、初始值是另一个vector对象的副本、所有元素的初始值都一样,但更多的时候是通过push_back动态添加元素

范围for语句体内不应改变其所遍历序列的大小

只有当元素的值可比较时,vector对象才能被比较

只要vector对象不是一个常量,就能向下标运算符返回的元素赋值

vector对象以及string对象的下标运算符可用于访问已存在的元素,而不是用于添加元素

迭代器:所有标准库容器都可以使用迭代器,但是其中只有少数几种才同时支持下标运算符

迭代器有有效和无效之分,有效的迭代器或者指向某个元素,或者指向容器中尾元素的下一位置

和指针不一样的是,获取迭代器不是使用取地址符,有迭代器的类型同时拥有返回迭代器的成员。比如,这些类型都拥有名为begin和end的成员,其中begin成员负责返回指向第一个元素的迭代器,end成员则负责返回指向容器“尾元素的下一位置”的迭代器,也被称为“尾后迭代器”

迭代器支持的运算符:*iter、iter->mem、++iter、iter1 == iter2、iter1 != iter2

迭代器类型:拥有迭代器的标准库类型使用iterator和const_iterator来表示迭代器的类型:

  • vector<int>::iterator it1;                 // it1能读写vector<int>的元素
  • string::iterator it2;                         // it2能读写string对象中的字符
  • vector<int>::const_iterator it3;     // it3只能读元素,不能写元素
  • string::const_iterator it4;              // it4只能读字符,不能写字符

如果vector对象或string对象是一个常量,只能使用const_iterator,如果vector对象或string对象不是常量,那么既能使用iterator也能使用const_iterator

begin和end返回的具体类型由对象是否是常量决定,如果对象是常量,begin和end返回const_iterator;如果对象不是常量,返回iterator。C++11新标准引入了两个新函数,分别是cbegin和cend

vector动态增长的副作用:不能在范围for循环中向vector对象添加元素;任何一种可能改变vector对象容量的操作都会使该vector对象的迭代器失效

内置数组:数组是一种复合类型,大小确定不变,不能随意向数组中增加元素,如果不清楚元素的确切个数,请使用vector

数组维度说明了数组中元素的个数,因此必须大于0。数组中元素个数也属于数组类型的一部分,编译的时候维度应该是已知的。也就是说,维度必须是一个常量表达式

和内置类型的变量一样,如果在函数内部定义了某种内置类型的数组,那么默认初始化会令数组含有未定义的值。Java数组会被初始化为零值(int[] ia = new int[3];)

  • int a[] = {0, 1, 2};        //维度是3的数组,元素值分别是0,1,2
  • 字符数组有一种额外的初始化方式,我们可以用字符串字面值对此类数组初始化,当用这种方式时,一定要记得字符串字面值结尾处还有一个空字符
  • char a1[] = {'C', '+', '+'};           // 列表初始化,没有空字符
  • char a2[] = {'C', '+', '+', '\0'};    // 列表初始化,含有显式的空字符
  • char a3[] = “C++";                  // 自动添加表示字符串结束的空字符
  • char a4[6] = “Daniel";            // 错误:没有空间可存放空字符!

数组不允许拷贝和赋值

  • int a[] = {0, 1,  2};        // 含有3个整数的数组
  • int a2[] = a;                 // 错误:不允许使用一个数组初始化另一个数组
  • a2 = a;                        // 错误:不允许把一个数组直接赋值给另一个数组

理解复杂的数组声明

  • int * ptrs[10];                      // ptrs是含有10个整型指针的数组
  • int &refs[10]; = /* ? */;        // 错误:不存在引用的数组
  • int (*Parray)[10] = &arr;     // Parray指向一个含有10个整数的数组
  • int (&arrRef)[10] = arr;       // arrRef引用一个含有10个整数的数组

要理解数组声明的含义,最好的办法是从数组的名字开始按照由内向外的顺序阅读

与vector和string一样,数组的下标是否在合理范围内由程序员负责检查,否则会有缓冲区溢出错误

数组特性:在很多使用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针。在大多数表达式中,使用数组类型的对象其实是使用一个指向该数组首元素的指针

  • int ia[] = {0, 1, 2, 3};        // ia 是一个整型数组
  • auto ia2(ia);                    // ia2 是一个整型指针,指向ia的第一个元素
  • ia2 = 42;                      // 错误:ia2是一个指针,不能用int值给指针赋值
  • decltype(ia) ia3 = {0, 1, 2, 3};    // 使用decltype关键字时上述转换不会发生,ia3还是数组类型

指向数组元素的指针也是迭代器,C++11新标准引入了begin和end函数,用于支持数组迭代

只要指针指向的是数组中的元素或尾后元素,都可以执行下标运算

  • int ia[] = {1,2,3,4};
  • int *p = &ia[2];
  • int j = p[1];                // p[1]等价于*(p+1),就是ia[3]表示的那个元素
  • int k = p[-2];              // p[-2]是ia[0]表示的那个元素

内置的下标运算符所用的索引值不是无符号类型,这一点与vector和string不同

对大多数应用来说,使用标准库string要比使用C风格字符串更安全、更高效,比如,strcat的目标字符串的大小还需要由调用者指定

C++语言中没有多维数组,通常说的多维数组其实是数组的数组

在初始化多维数组时并非所有元素都必须包含在初始化列表中,如果仅仅想初始化每一行的第一个元素,通过如下的语句即可:

int ia[3][4] = { {0, 1}, {4}, {8}};

其他未列出的元素执行值初始化

使用范围for语句处理多维数组时,外层循环的控制变量一定要声明成引用类型,这样能够避免数组被自动转成指针

int (*p)[4] = ia;   // p指向含有4个整数的数组

表达式由一个或多个运算对象组成,对表达式求值将得到一个结果,字面值和变量是最简单的表达式,其结果就是字面值和变量的值。把一个运算符和一个或多个运算对象组合起来可以生成较复杂的表达式

组合运算符和运算对象:对于含有多个运算符的复杂表达式来说,要想理解它的含义首先要理解运算符的优先级结合律以及运算对象的求值顺序。括号无视优先级与结合律

运算对象转换:在表达式求值的过程中,运算对象常常由一种类型转换成另外一种类型。尽管一般的二元运算符都要求两个运算对象的类型相同,但是很多时候即使运算对象的类型不同也没有关系,只要它们能被转换成同一种类型即可

重载运算符:当运算符作用于类类型的运算对象时,用户可以自行定义其含义,这种自定义的过程事实上是为已存在的运算符赋予了另外一层含义,所以称之为重载运算符。我们使用重载运算符时,其包括运算对象的类型和返回值的类型,都是由该运算符定义的,但是运算对象的个数、运算符的优先级和结合律是无法改变的

左值和右值:当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置),接下来在介绍运算符的时候,我们会注明该运算符的运算对象是否必须是左值以及其求值结果是否是左值

求值顺序:逻辑与运算符、逻辑或运算符、条件运算符(?:)、逗号运算符,只有这4种运算符明确规定了求值顺序

算术运算符:满足左结合律,运算对象和求值结果都是右值。除非另作特殊说明,算术运算符都能作用于任意算数类型以及任意能转换为算术类型的类型。在除法运算中,如果两个运算对象的符号相同则商为正,否则商为负,商一律向0取整

逻辑运算符:短路求值,运算对象和求值结果都是右值

关系运算符:满足左结合律,但很少写出 i < j < k 的语句

赋值运算符:满足右结合律。左侧运算对象必须是一个可修改的左值,求值结果为左侧运算对象,并且是一个左值。如果赋值运算符的左右两个运算对象类型不同,则右侧运算对象将转换成左侧运算对象的类型

C++11新标准允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象,如果左侧运算对象是内置类型,那么初始值列表最多只能包含一个值,而且该值即使转换的话其所占的空间也不应该大于目标类型的空间;对于类类型来说,赋值运算的细节由类本身决定。对于vector来说,vector模版重载了赋值运算符并且可以接收初始值列表,当赋值发生时用右侧运算对象的元素替换左侧运算对象的元素。无论左侧运算对象的类型是什么,初始值列表都可以为空,此时,编译器创建一个值初始化的临时量并将其赋给左侧运算对象

递增递减运算符:作用于左值运算对象,前置版本将对象本身作为左值返回,后置版本则将对象原始值的副本作为右值返回

成员访问运算符:箭头运算符作用于一个指针类型的运算对象,结果是一个左值。点运算符分成两种情况:如果成员所属的对象是左值,那么结果是左值,反之是右值

条件运算符:满足右结合律。当条件运算符的两个表达式都是左值或者能转换成同一种左值类型时,运算的结果是左值;否则运算的结果是右值

位运算符:满足左结合律。如果运算对象是带符号的且它的值为负,那么右移运算符如何处理运算对象的“符号位”依赖于机器。而且,此时左移操作可能会改变符号位的值,因此是一种未定义的行为,所以强烈建议仅将位运算符用于处理无符号类型

sizeof运算符:sizeof不实际计算其运算对象的值,因此我们可以使用作用域运算符或者通过解引用无效指针来获取类成员的大小,其中,对引用类型执行sizeof运算得到被引用对象所占空间的大小,对指针执行sizeof运算得到指针本身所占空间的大小,对数组执行sizeof运算相当于对数组中所有元素各执行一次sizeof运算并将所得结果相加,sizeof运算不会把数组转换成指针来处理,对string对象或vector对象执行sizeof运算只返回该类型固定部分的大小(总分配的大小),不会计算对象中的元素占用了多少空间。sizeof返回值是一个常量表达式

逗号运算符:首先对左侧的表达式求值,然后将求值结果丢弃掉,逗号运算符真正的结果是右侧表达式的值。如果右侧运算对象是左值,那么最终的求值结果也是左值

算术转换

整型提升负责把小整数类型转换成较大的整数类型。对于bool、char、signed char、unsigned char、short和unsigned short等类型来说,只要它们所有可能的值都能存在int里,它们就会提升成int类型;否则,提升成unsigned int类型。较大的char类型(wchat_t、char16_t、char32_t)提升成int、unsigned int、long、unsigned long、long long和unsigned long long中最小的一种类型

如果一个运算对象是

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

little-sparrow

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值