2.变量和基本类型
c++的算术类型:
类 型 | 含 义 | 最小尺寸 |
---|---|---|
bool | 布尔类型 | 未定义 |
char | 字符 | 8位 |
wchar_t | 宽字符 | 16位 |
char16_t | Unicode字符 | 16位 |
char32_t | Unicode字符 | 32位 |
short | 短整型 | 16位 |
int | 整型 | 16位 |
long | 长整型 | 32位 |
long long | 长整型 | 64位 |
float | 单精度浮点数 | 6位有效数字 |
double | 双精度浮点数 | 10位有效数字 |
long double | 扩展精度浮点数 | 10位有效数字 |
一个char和一个机器字节一样,一个int至少和一个short一样大,一个long至少和一个int一样大。
通常float以一个字(32 bit)表示,double以2个字来表示,分别有7个和16个有效位。
8位unsigned char 可以表示0到255的值,signed char可以表示-128 到 127。
- 多数平台上int类型占32位,取值范围为-2147483648 至 2147483647
类型转换
bool b = 42; // b = true
int i = b; // i = 1
i = 3.14; // i = 3
double pi = i; // pi = 3.0
unsigned char c = -1; // 若char占8bit,则c = 255
signed char c2 = 256; // 若char占8bit,c2的值是未定义的
- 把非bool类型的算术值给bool类型时,初始为0则结果为false,否则结果为true
- 把bool类型赋值给非bool类型时,初始值为false则结果为0,初始值为true则结果为1
- 把浮点数赋值给整型时,只保留整数部分
- 整数给浮点数时,小数部分记0
- 给无符号类型一个超出其表示范围的值时,结果的初始值对无符号类型表示数值总数取模后的余数,例如给8bit的unsigned char(可以表示0到255)赋值-1,结果为-1%256=255
- 给带符号的类型赋值一个超出它表示范围的值时,结果是未定义(undefined)的,可能会出错
注意! 带符号数与无符号数运算时,会被转化为unsigned,int a = -1可能会变成4294967295,切勿混用unsigned类型和signed类型!
字面值
O开头的数代表八进制数,0x开头的数表示十六进制,例如整数20:
20 // 十进制
O24 // 八进制
0x14 // 十六进制
一般复数的那个负号不会存储在字面值内,作用仅仅是对字面值取负
转义序列:
:-:|:-:|:-:
换行符 \n| 横向制表符 \t |报警符 \a
纵向制表符 \v| 退格符 \b| 双引号 "
反斜线\ |问号 ? |单引号'
回车符 \r |进纸符 \f|
也可以通过添加前后缀来指定字面值的类型:
对于字符有:
前缀 | 含义 | 类型 |
---|---|---|
u | Unicode 16字符 | char16_t |
U | Unicode 32字符 | char32_t |
L | 宽字符 | wchar_t |
u8 | UTF-8(仅用于字符串字面值常量) | char |
对于整型和浮点型有:
后缀 | 类型 |
---|---|
u or U | unsigned |
l or L | long |
ll or LL | long long |
f or F | float |
l or L | long double |
变量
变量定义的基本形式:
类型说明符 + 一个或多个变量名组成的列表(以逗号分隔,分号结束)
-
何为对象?
对象(object)是指一块能存储数据并具有某种类型的内存空间
-
对象初始化
当对象在创建时获得了一个特定的值,那么这个对象被初始化了,在同一条语句中可以用先定义的变量值去初始化后定义的变量。
double price = 100.1, discount = price * 0.16;
注意初始化不是赋值!初始化=创建变量的同时赋予其一个初值,赋值=擦除对象的当前值以一个新的值来代替。
-
默认初始化
变量将被初始化为默认值,也有可能定义在函数体内部的内置类型变量不被初始化
建议初始化每一个内置类型的变量!
声明和定义的关系 *
c++采用分离式编译机制,允许将程序分割为若干个文件,每个文件可以被独立编译
为了支持分离式编译,c++将声明和定义区分开
声明(declaration)使名字为程序所知
定义(definition)负责创建与名字关联的实体
变量声明规定了变量的类型和名字,定义在这方面也与之相同。但是定义还申请存储空间,也可能会为变量赋一个初值。
如果想声明一个变量而非定义它,需要用到extern关键字,而且不要显示初始化变量。任何包含了显示初始化的声明即成为定义!并且变量只能被定义一次,可以被多次声明。
extern int i; //声明 i 而非定义 i
int j; //声明并定义 j
extern int k = 1;//定义 k
!如果要在多个文件中使用同一个变量,就必须把声明和定义分离。此时,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,不能重复定义。
- c++是一种静态类型语言,其含义是在编译阶段检查类型。其中检查类型的过程称为类型检查。我们在使用某个变量前必须声明其类型。
标识符
由字母、数字、下划线组成。必须以字母和下划线开头。
int somename, someName, SomeName, SOMENAME;
并且还有一些保留字。用户自定义的标识符中不能出现连续两个下划线,也不能以下划线加大写字母开头。定义在函数体外的标识符不能以下划线开头。
int _ = 1; //是正确的
名字的作用域
作用域以花括号分隔。
定义在函数体之外的名字具有全局作用域,例如main。
相对的块作用域
复合类型
基本数据类型 + 类型修饰符 + 变量名
int i = 1024, *p = &i, &r = i;
最好把类型修饰符和变量名写在一起
-
引用(左值引用)
引用即别名, 引用必须被初始化
-
指针
定义多个指针时,必须每个变量前面都有符号 *
指针存放某个对象的地址,要想获取该地址,要使用取地址符(&)
double dval; double *pd = &dval; double *pd2 = pd; int *pi = pd; //错误,指针pi的类型和pd不匹配 pi = &dval; //错误,指针pi的类型和dval不匹配
通过解引用符(*)来得到指针所指的对象。
!对于符号(*, &),在定义时,作为声明的一部分;同时也可以作为解引用符和取地址符。
生成一个空指针:
int *p1 = nullptr; int *p2 = 0; int *p3 = NULL;
建议初始化所有指针!
把指针用于条件表达式时,任何非0指针的条件值都是true。
-
void * 指针
可以用于存放任意对象的地址,但也只能存储地址,不能访问内存空间中的对象。
-
指向指针的指针
可以有指向指针的指针的...指针
-
指向指针的引用
相当于对指针的引用
int i = 42; int *p; int *&r = p; // r 是对指针 p 的引用
要理解 *&r 到底是什么,应该从右往左看类型修饰符,距离r最近的符号是&,那么r就是一个引用。其余的符号确定r引用的类型是什么,符号*说明r引用的是一个指针。最后int类型说明r引用的是一个int类型的指针。
从右往左看,很重要!
const限定符
const类型的对象一旦创建后其值就不能再改变,所以const对象必须初始化。
可以用const对象去初始化其他对象,因为初始化操作不会改变本身的值,只是拷贝了值。
int i = 42;
const int ci = i;
int j = ci;
! 默认状态下const对象仅在文件内有效,因为在编译过程中,会把会用到该变量的地方都替换成对应的值。
当在多个文件中,出现同名的const变量时,等同于在不同文件中分别定义了不同的变量,都需要进行初始化。
如果在多个文件中,都要用到同一个值的const变量,而且这一变量的初始值不是一个常量表达式,又确实有必要在文件间共享。一个解决办法就是,对于 const 变量不管声明还是定义都添加 extern 关键字,这样就只需要定义一次就够了。
// file_1.cc中定义并初始化一个常量
extern cosnt int bufSize = fcn();
// file_1.h头文件
extern cosnt int bufSize;
const的引用
称为对常量的引用,也不能修改它所绑定的对象。
const int ci = 100;
const int &r1 = ci; //正确
r1 = 42; //错误,不能修改const对象
int &r2 = ci; //错误,试图让一个非常量引用指向一个常量对象
严格来讲并不存在常量引用。
对const的引用可能引用一个并非const的对象。
int i = 42;
int &r1 = i;
const int &r2 = i; //r2绑定对象i,但是不允许通过r2修改i的值
r1 = 0; // 通过r1改变了i的值,同时会引起r2的改变
r2 绑定非常量整数 i 是合法的行为。
指针和const
要想存放常量对象的地址,只能用指向常量的指针,也不能赋值。
const double pi = 3.14159;
const double *ptr = π
同时指向常量的指针也能指向一个非常量。所谓指向常量的指针,只是不能通过这个指针来改变对象的值,但是没有规定那个对象的值不能通过其他途径改变。
const指针
允许把指针本身定为常量,常量指针必须初始化,而且一旦初始化完成它的值(存放的地址)就不能再改变了。
int errNumb = 0;
int *const curErr = &errNumb; //curErr一直指向errNumb
const double pi = 3.14159;
const double *const pip = *pi; //指向常量对象的一个常量指针
指针本身是个常量,可以改变它指向的对象的值!
顶层const
顶层const 表示指针本身是个常量
底层const 表示指针所指的对象是个常量
指针也可以同时是底层和顶层const
int i = 0;
int *const p1 = &i; //p1的值不能改变,顶层const
const int ci = 42; //ci的值不能改变,顶层const
const int *p2 = &ci; //p2的值可以改变,底层const
const int *const p3 = p2; //靠右的const是顶层,靠左的cosnt是底层
const int &r = ci; //用于声明引用的const都是底层cosnt
在执行拷贝操作时,顶层cosnt不受影响。
另一方面,拷入和拷出的对象必须具有相同的底层const资格,或两个对象的数据类型必须能够转换(一般非常量可以转换成常量)
就是说可以用一个 const int &
去绑定一个普通的int
对象
类型别名
两种方法定义类型别名:
typedef double wages; //wages = double
typedef wages base, *p; //base = double, p = double*
using SI = Sales_item; //SI = Sales_item
注意不是简单的替换:
typedef char *pstring;
const pstring cstr = 0; //cstr是指向char的**常量指针**
const pstring *ps; //ps是一个指针,它指向的对象是指向char的常量指针
但是上述语句不是简单的替换就行的,不等于以下语句:
const char *cstr = 0; //这是指向常量字符的指针,和上面的不同!!!
decltype
类型说明符decltype,作用是返回操作数的数据类型(包括顶层const和引用在内)
const int ci = 0, &cj = ci;
decltype(ci) x = 0; //正确,x是一个const int类型
decltype(cj) y = x; //正确,y是一个const int&,绑定到x
decltype(cj) z; //错误,z是一个const int&,必须初始化
注意decltype对括号很敏感,如果decltype使用的是一个不加括号的变量,得到的结果就是该变量的类型。如果给变量加上了一层括号,这个变量就变成了一个表达式,表达式是可以作为赋值语句左值的,因此是一个引用类型。
int i = 0;
decltype((i)) d; //d是int&,必须初始化,报错
decltype(i) e; //e是int类型
!!!双层括号 decltype((i))
得到的结果永远是一个引用,一层括号只有当里面是引用的时候才是引用。
预处理器
一项预处理功能
#include
,当看到#include
标记时,会用指定的头文件内容代替它。-
头文件保护符依赖于预处理变量,
#define
把一个名字设定为预处理变量,#ifdef
,#ifndef
分别当且仅当变量已定义时、未定义时为真,这两个if直到#endif
指令为止。#ifndef SALES_DATA_H #define SALES_DATA_H #include <string> struct Sales_data{ ...; } #endif
!!预处理变量无视作用域规则
3.字符串、向量和数组
命名空间
使用using声明
using namespace::name;
//例如 using std::cin;
using std::endl;
作用域操作符 :: ,含义是编译器从操作符左侧名字所示的作用域中寻找右侧的那个名字。例如 std::cin
就是使用命名空间 std 中的名字 cin 。
一般来说,头文件不应该包含 using 声明
标准库类型string
#include<string>
定义和初始化:
string s1; //默认初始化,s1是一个空串
string s2 = s1; //两种拷贝初始化
string s22(s1);
string s3 = "hiya"; //s3是该字符串字面值的副本
string s33("hiya");
string s4(10, 'c'); //s4是10个字符c组成的字符串
string的操作:
os<<s //流操作,将s写到输出流os中,返回os
is>>s //从is中读取字符串赋给s,字符串以空白分隔,返回is
getline(is, s) //从is中读取一行赋给s,返回is
s.empty()
s.size() //返回字符的个数!!!!用size
s[n] //返回第n个字符的引用
s1 + s2 //返回s1与s2连接后的结果
s1 = s2
s1 == s2
s1 != s2
<, >, <=, >=
如果使用 cin>>str
时,会自动忽略开头的空白(空格,换行,制表符等),从第一个真正字符开始,直到遇见下一个空白为止。!!以空格为分界!!
例如输入" Hello world "
,会得到"Hello"
读取未知数量的string对象:
string word;
while(cin >> word){
cout << word << endl;
}
读取一整行:
string line;
while(getline(cin, line)){
cout << line << endl;
}
getline
得到的一行中不包括换行符,得手动加上 endl。 触发getline函数返回的那个换行符实际被丢掉了。
-
size()
函数返回的是一个size_type
类型,无符号数!!!!注意,如果表达式中已经有了 size() 类型,就不要用带符号的 int 类型了,否则会转化成无符号数产生意想不到的后果。
或者可以:
auto len = line.size();
string 的大小比较是对字典序进行比较,对大小写敏感,'A' = 65, 'a' = 97
当把字符串和字面值相加时,必须保证每个 '+' 两侧至少有一个是 string 类型! 字符串字面值和 string 是两种不同的类型!!!
-
处理 string 中的字符
使用cctype头文件
isalnum(c) //是字母或数字 isalpha(c) //是字母 iscntrl(c) //是控制字符 isdigit(c) //是数字 islower(c) //是小写字母 isupper(c) //是大写字母 ispunct(c) //是标点符号 ... tolower(c) toupper(c)
-
c++ 版本的C标准库头文件
c语言的头文件形如
name.h
,而c++则将这些名字去掉了 .h ,相应的增加了 c 字母前缀,变成cname
。 例如:#include <cmath> #include <cctype>
-
处理字符
c++11 新的范围for语句:
for (declaration : expression) statement; for (auto c : str) cout<< c <<endl; for (auto &c : str) // 如果要修改字符,必须要引用 c = toupper(c);
下标操作([ ]):
第一个字符
s[0]
, 最后一个字符s[s.size() - 1]
e.g. 把字符串 s 的第一个词改成大写形式:
for(decltype(s.size()) index = 0; index != s.size() && !isspace(s[index]); ++index) s[index] = toupper(s[index]); //s = "some string", 结果s = "SOME string"
-
reverse()
函数c++中,
reverse()
函数是在 <algorithm> 中定义的,用于反转在 [first,last) 范围内的顺序。可以用于反转 vector 类型,或者 string 类型等。
vector<int> v={1,2,3,4,5}; reverse(v.begin(),v.end());//v的值为5,4,3,2,1 string str="C++REVERSE"; reverse(str.begin(),str.end());//str结果为ESREVER++C
标准库类型 vector
vector(向量),有时也被称为容器(container)。vector 实际是一个类模板,编译器根据模板创建类或者函数的过程称为实例化,通过在模板名字后加上尖括号,在尖括号内填放信息。
vector<int> ivec;
vector<Sales_item> Sales_vec;
vector<vector<string>> file; // 在早期版本中,要在后面的两个>>中加上一个空格,写成下列形式:
vector<vector<string> > file;
初始化:
vector<T> v1;
vector<T> v2(v1);
vector<T> v2 = v1;
vector<T> v3(n, val);
vector<T> v4(n); // v4包含了n个重复地执行了值初始化的对象
vector<T> v5{a, b, c...}; //列表初始化
vector<T> v5 = {a, b, c...}; //效果同上,这是c++11的新标准
可以进行默认初始化,不含任何元素。
在进行值初始化时,可以不加值具体是什么,只写入数字,例如 vector<T> v4(n)
当使用的模板是 int 时,元素初值自动设为0。
!!注意使用花括号和圆括号,这两个影响非常大。花括号是进行列表初始化,圆括号是按提供的 (n, val) 来构造 vector 对象。
-
添加元素
push_back(..) 把元素添加到尾端
!!注意,虽然vector对象能高效增长,定义vector对象的时候设定其大小也就没什么必要了,但事实上这么做性能可能更差!
在创建vector时,预先设定其容量是最好的。
!! 范围for语句体内不应改变其所遍历的序列的大小,不能在里面对vector进行添加元素等操作。
-
对vector的操作
v.empty() v.size() // 返回vector中的元素个数,size和empty操作和string类的一样 v.push_back(t) v[n] v1 = v2 v1 = {a, b, c, ...} //同列表中的元素拷贝替换v1中的元素 v1 == v2 v1 != v2 <, <=, >, >= //按**字典序**进行比较
size()
返回的是由 vector 定义的 size_type 类型,注意要使用vector<int>::size_type // 正确 vector::size_type // 错误,要指定类型
只能对确知已存在的元素执行下标操作!
迭代器
所有标准库容器都可以使用迭代器访问,但是只有少数才同时支持下标运算符。string和vector都同时支持这两种访问方式。
严格来讲string不属于容器类型,但是很多容器类型的操作string都支持。
获取迭代器使用 begin 和 end 成员。 其中 begin 指向第一个元素,end 表示尾元素的下一个元素的位置,如果为空,则有 begin == end
。 end 称为尾后迭代器,简称尾迭代器。
-
迭代器的运算符(类似于指针):
*iter iter->mem //解引用 iter 并获取该元素的名为 mem 的成员,等价于 (*iter).mem ++iter --iter iter1 == iter2 iter1 != iter2
尽量习惯使用迭代器以及!=运算符,不是所有的标准库容器都支持<运算的,但是都支持迭代器和!=操作。
-
迭代器类型:
那些拥有迭代器的标准库类型使用 iterator 和 const_iterator 来表示迭代器的类型。
vector<int>::iterator it; string::iterator it2; vector<int>::const_iterator it3; // it3 只能读取元素,不能写元素 string::const_iterator it4; // it4 只能读取元素,不能写元素
const_iterator 类型和常量指针差不多,能读取但不能修改它所指的元素值。
如果 vector 对象或 string 对象是一个常量,只能使用 const_iterator 。
!! 对于常量的容器类型,begin 和 end 运算符得到的是一个 const_iterator ,要用对应的 const_iterator 接住。如果一定要从非常量容器中得到常量迭代器,可以使用
cbegin
和cend
这两个运算符。 -
箭头运算符 ->
it -> mem
的含义是(*it).mem
包含了对 it 进行解引用,以及点运算符。例如一个 vector<string> 类型的 text ,用迭代器访问遍历 text,直到遇到空白字符串为止:
for(auto it = text.cbegin(); it != text.cend() && !it -> empty(); ++it) cout << *it << endl;
其中
!it -> empty()
等价于!(*it).empty()
。先解引用得到 string 类型,再判断这个字符串是否为空。
!! 但凡使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
-
string 和 vector 的迭代器的其他运算
iter + n iter - n iter1 += n iter1 -= n iter1 - iter2 //相减得到迭代器之间的距离 >, >=, <, <=
也可以进行一些算术运算,例如得到中间元素:
auto mid = vi.begin() + vi.size() / 2;
这个距离是一个 difference_type ,带符号整数,因为距离可正可负。
数组
数组的大小确定不变
数组的声明形如 a[d]
,其中a是数组名字,d是数组维度,维度必须是一个常量表达式。不允许使用 auto 关键字,必须指定数组的类型。和vector一样,数组的元素为对象,不存在引用的数组。
初始化数组时可以忽略维度,根据初始值数量计算并推测出维度。
显式初始化:
const unsigned sz = 3;
int ia1[sz] = {0, 1, 2};
int a2[] = {0, 1, 2};
int a3[5] = {0, 1, 2}; // 等价于 a3[5] = {0, 1, 2, 0, 0}
string a4[3] = {"hi", "bye"}; // 等价于 a4[3] = {"hi", "bye", ""}
int a5[2] = {0, 1, 2}; // 这句错误,初始值过多
对于字符数组,还可以用字符串来进行初始化:
char a3[] = "c++"; // 等价于 char a3[] = {'c', '+', '+', '\0'}
!!注意这里有一个 '\0' 结尾存放空字符,a3的实际大小为4。
数组不允许拷贝和赋值 (也有一些编译器能支持,但是最好不要使用这些非标准特性)
-
复杂数组的声明
类型修饰符遵循从右向左结合的原则。
int *ptrs[10]; // ptrs是含有10个整型指针的数组 int &refs[10] = ...; // 错误,不存在引用的数组 int (*Parray)[10] = &arr; // Parray指向一个含有10个整数的数组 int (&arrRef)[10] = arr; // arrRef引用一个含有10个整数的数组
对于 ptrs,首先[10],它是一个大小为10的数组,其次 * 表示它存放的是指针,int 是存放的指针指向的类型;
对于 Parray,最好由内向外地去理解它的含义。圆括号内的 *Parray 表示 Parray 是一个指针,接下来右边的 [10] 可以知道 Parray 这个指针指向大小为 10 的数组,最左边的 int 表示数组的数据类型。
对于 arrRef,同理,它是一个引用,引用的内容是一个含有10个整数的数组。
更复杂的:
int *(&arry)[10] = ptrs;
arry 是一个对数组的引用,引用的数组大小为10,存放的int类型的指针。
-
访问数组元素
下标访问方式,数组的下标是一个
size_t
类型,它是一种机器相关的无符号类型,足够大以便能表示内存中任意对象的大小。其他的下标操作类似于 vector
-
指针和数组
使用数组的时候编译器一般都会把它转换成指针,可以用取地址符来得到指向数组元素的指针:
string nums[] = {"one", "two", "three"}; string *p = &nums[0]; // p 指向 nums 的第一个元素 string *p = nums; // 等价于上面一句
在大多数表达式中,使用数组类型的对象实际是使用一个指向该数组首元素的指针。
一般数组名 nums 都会被转化成 &nums[0],除了使用 decltype(nums) 的时候,会返回一个 string[3] 类型。
对于空指针,允许给它加上或减去一个值为 0 的整型常量表达式,两个空指针也允许彼此相减,当然结果为 0
-
标准库函数 begin 和 end
数组不是类型,因此 begin 和 end 不能作为成员函数。使用方法:
int ia[] = {0,1,2,3,4,5,6,7,8,9}; int *beg = begin(ia); int *last = end(ia);
begin 函数返回指向 ia 首元素的指针, end 函数返回指向 ia 尾元素的下一位置的指针。
!! 标准库类型的下标运算符使用的索引值必须是无符号类型,而内置的下标运算符不同,可以是负值。例如:
int ia[] = {0, 2, 4, 5, 8};
int *p = &ia[2]; // p指向索引为2的元素
int i = *(p + 2);
int k = p[-2]; // k为ia[0],索引值可以为负
多维数组
多维数组 = 数组的数组..
定义一个数组元素任然为数组的数组时,用两个维度来定义它,一个表示数组本身大小,一个表示其元素的大小。
int ia[3][4]; // 大小为3的数组,每个元素含有4个整数
int arr[10][20][30] = {0};
对于二维数组,第一个维度成为行,第二个维度称为列。
int ia[3][4] = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11} // 注意内部是逗号,括号都是花括号
};
也可以写成一行的形式:
int ia[3][4] = {0, 1, 2, 3, 4, 5, 6,..., 11};
只初始化每一行的第一个元素:
int ia[3][4] = {{0}, {4}, {8}};
初始化第一行:
int ix[3][4] = {0, 3, 6, 9}; // 加不加花括号差别很大,对比上面一句
-
下标引用
如果给出的下标和维度一样多,那么就是这一点的元素值。如果缺少,就是给定索引处的一个内层数组。
int (&row)[4] = ia[1]; // row 是一个4个int的数组的一个引用,绑定 ia 的第二个4元数组上,也就是第二行。
!! 对于
ia[1]
,因为 ia 是一个 int [3][4] 的类型,从左往右看,ia[1]应该是一个 int [4] 的类型,是一个四元数组。在使用范围 for 语句处理二维数组时,对于外层数组一定要写引用符号,否则无法通过编译:
for(auto row : ia) for(auto col : row) //无法通过编译 for(const auto &row : ia) for(auto col : row) //可以运行
!! 除了最内层循环外,其他所有循环的控制变量都应该是引用类型
-
指针和多维数组
由多维数组名转化来的指针实际上是指向第一个内层数组的指针。
int ia[3][4]; int (*p)[4] = ia; // p 指向含有4个整数的数组 p = &ia[2]; // p 指向 ia 的尾元素
使用指针遍历:
for(auto p = ia; p != ia + 3; ++p){ for(auto q = *p; q != *p + 4; ++q) cout << *q; cout << endl; } // 也可以更简洁: for(auto p = begin(ia); p != end(ia); ++p){ for(auto q = begin(*p); q != end(*p); ++q) cout << *q; cout << endl; } // 这里 auto 等价于 int[4] *
4.表达式
重载运算符可以改变运算对象的类型和返回值的类型,但是运算对象的个数、运算符的优先级和结合律无法改变。
-
左值和右值
rvalue 和 lvalue
左值可以位于赋值语句的左侧,而右值不能。当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)
在需要用右值的地方可以用左值代替,但是不能把右值当成左值使用。
- 赋值运算符左侧需要一个(非const)左值作为运算对象,得到的结果仍然是一个左值。
- 取地址符作用于一个左值对象,返回一个指向该运算对象的指针,这个指针是个右值。
-
求值顺序
4种明确规定了运算对象求值顺序的运算符:
- 逻辑与(&&) 先求左侧运算对象的值,只有左侧运算对象的值为真时才继续求右侧运算对象的值
- 逻辑或(||) 先求左侧,左侧为假时才求右侧表达式
- 条件运算符(?:)
- 逗号运算符(,)
算术运算符
一元正负号具有最高优先级,其次* / %,最低的是+ -,.
c++11中,除了 -m 导致溢出的情况,其他时候都有 (-m)/n 和 m/(-n) 都等于 -(m/n),m%(-n) 等于 m%n, (-m)%n 等于 -m%n
也就是: (-21)%(-8) = (-21)%8 = -21%8 = -5
逻辑运算符
逻辑与和逻辑或都遵循先计算左侧表达式的原则(短路求值)。他们的左侧运算对象一般用来保证右侧求值过程中的正确性和安全性,例如:
index != s.size() && !isspace(s[index])
s.empty() || s[s.size() - 1] == '.' // 遇到空字符串或以句号结束的字符串
!!在进行比较运算时除非比较的对象是布尔类型,否则就不要用布尔类型的字面值 true 和 false 作为运算对象。
赋值运算符
赋值运算符的优先级比较低,在表达式中尽量加上括号。
允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象:
int k = {3.14}; // 错误,进行了窄化转换
vector<int> vi = {0,1,2,3,4}; // 正确, vi 包含 5 个元素
赋值语句满足右结合律,赋值语句返回的是左侧运算对象:
int ival, jval;
ival = jval = 0; // 都被初始化为0
其他复合赋值运算符:(都等价于a = a op b)
+= -= *= /= %=
<<= >>= &= ^= |=
递增和递减运算符
++和--有前置和后置之分。
int j, i = 0;
j = i++; // 后置版本得到递增之前的值, j=0, i=1
j = ++i; // 前置版本得到递增之后的值, j=2, i=2
前置版本将对象本身作为左值返回,后置版本则将对象原始值的副本作为右值返回。
!! 除非必须,否则不用后置版本的递增递减运算符。因为后置版本同时存储了修改前后的两个值,如果不需要用修改前的值的话,后置的递增、递减就造成一种浪费。
后置的++也可以用来输出前一个元素,例如循环输出一个vector对象内容直至遇到(但不包括)第一个负值为止:
auto pbeg = v.begin();
while(pbeg != v.end() && *beg >= 0)
cout << *pbeg++; // 后置++,每次解引用输出的都是迭代器的前一个
++ 的优先级比解引用 * 高,因此 *pbeg++ 等价于 *(pbeg++)
后置的 ++ 操作返回值是未自增前的那个 pbeg,因此 pbeg 自增后,解引用的仍然是原来没自增的那个迭代器。
!!注意 *pbeg++
这种写法,常用。
成员访问运算符
ptr->mem
等价于 (*ptr).mem
解引用运算符的优先级低于点运算符!所以上式必须加括号。
箭头运算符作用于一个指针类型的运算对象,结果是一个左值。点运算符的结果则根据成员所属的对象是左值和右值而定。
条件运算符
简单的 if-else 语句可以用条件运算符(?:)实现
条件运算符可以嵌套:
finalgrade = (grade > 90) ? "high pass"
: (grade < 60) ? "fail" : "pass";
最好不要嵌套太多层。
!!在输出表达式中,由于条件运算符的优先级非常低,因此两边一般要加上括号。
cout<< ((grade < 60) ? "fail" : "pass") <<endl;
位运算符
运算符 | 功能 |
---|---|
~ | 位求反 |
<< | 左移 |
>> | 右移 |
& | 位与 |
^ | 位异或 |
| | 位或 |
!!对符号位的处理没有规定,因此强烈建议仅对无符号类型进行位运算
sizeof运算符
两种形式:
sizeof(type)
sizeof expr
sizeof 满足右结合律,且优先级和 * 一样。有 sizeof *p 也就是 sizeof(*p)。
因为sizeof实际不会去求表达式的值,所以即使 p 是一个无效(未初始化)指针也无所谓。
- 对 char 类型或者类型为 char 的表达式执行 sizeof 运算,结果得 1
- 对引用类型执行 sizeof 运算,得到被引用对象所占空间的大小
- 对指针执行 sizeof 运算,得到指针本身所占空间大小
- 对解引用指针执行 sizeof 运算,得到指针指向的对象所占空间的大小,指针不需要有效
- 对数组执行 sizeof,得到整个数组所占空间的大小,sizeof不会把数组转化为指针处理
- 对 string 对象或 vector 对象运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间
类型转换
如果两种类型可以相互转换,那么他们就是关联的。
-
隐式转换:
- 在大多数表达式中,比 int 类型小的整型值首先提升为较大的整数类型
- 在条件中,非布尔值转换成布尔值
- 初始化过程中,初始值转化成变量的类型;赋值语句中,右侧的对象类型转换成左侧的
- 算术运算和关系运算中,多种类型都转换成同一种类型
- 函数调用也会发生类型转换
- 数组转换成指针,但是在 &、sizeof、decltype下不会转化
-
整型提升:
负责把小整数类型转换成较大的整数类型。对于bool、char、signed char、unsigned char、short和unsigned short,只要他们所有可能的值都能存在 int 里,他们就会被提升成 int 类型。
较大的 char 类型(wchar_t、char16_t、char32_t)提升成int、unsigned int、long、unsigned long、long long和unsigned long long 中最小的一种类型。
在计算时,首先进行整型提升,如果类型匹配则进行计算,如果提升后的两个运算对象一个带符号一个无符号,且无符号类型不小于带符号的,则带符号的会被转化成无符号的,例如unsigned int 和 int 会使 int 转化成无符号的。
-
显示转换:
一个命名的强制类型转换具有如下形式:
cast-name<type>(expression);
cast-name有四种:
-
static_cast: 任何不包含底层const的都可以使用
double slope = static_cast<double>(j) / i;
-
const_cast: 只能改变运算对象的底层const,“去掉const性质”,一般用于有函数重载的上下文中
const char *pc; char *p = const_cast<char*>(pc); // 正确,但是不能通过 p 写值
dynamic_cast
reinterpret_cast:通常为运算对象的位模式提供较低层次上的重新解释
!!尽量不要使用强制类型转换
-
5.语句
-
空语句:只包含一个单独的分号,最好加上注释
也可以写一个空块:
while(cin >> s && s != sought) { } // 空块 while(cin >> s && s != sought) ; // 空语句
-
悬垂 else :
当一个if语句嵌套在另一个if语句中时,可能if分支会多于else分支。c++规定else与离它最近的尚未匹配的if匹配,这点和python不同!
最好用花括号控制执行路径:
if(grade % 10 >= 3) { // 花括号必不可少! if(grade % 10 > 7) lettergrade += '+'; } else lettergrade += '-';
-
switch 语句:
switch (ch) { case 'a': ++aCnt; break; case 'b': ++bCnt; break; ... default: ++otherCnt; break; }
如果某个 case 标签匹配成功,将从该标签往后顺序执行所有 case 分支,如果不 break,直到 switch 结束时才会停下来。
或者把他们都写在一行里也是一种合法的语句。
switch (ch) { // 只要是5个字母中的任意一个都会执行后面的++ case 'a': case 'e': case 'i': case 'o': case 'u': ++vowelCnt; break; } switch (ch) { case 'a': case 'e': case 'i': case 'o': case 'u': ++vowelCnt; break; } // 一般不要省略分支后面的break语句,如果要省略,最好加上注释!
如果需要为某个 case 分支定义并初始化一个变量,应该把变量定义在块内,加上一个 { }
-
跳转语句
break 语句负责终止离他最近的 while、 do while、 for 或 switch 语句。仅限于最近的一个循环或者 switch,只跳出一层循环。
continue 语句终止最近的循环的当前迭代,并立即开始下一次循环。
-
try 语句块
异常处理包括: throw 表达式,引发异常; try 语句块,处理异常,以关键字 try 开始,以一个或多个 catch 子句结束,称为异常处理代码;一套异常类,用于在 throw 表达式和相关的 catch 子句之间传递异常的具体信息。
抛出异常:
if(item1.isbn() != item2.isbn()) throw runtime_error("Data must refer to same ISBN"); cout << item1 + item2 <<endl;
异常处理代码:
try { ... if(item1.isbn() != item2.isbn()) throw runtime_error("Data must refer to same ISBN"); ... } catch(runtime_error err) { cout << err.what() << "\nTry again? Enter y or n: " <<endl; char c; cin >> c; if(!cin || c == 'n') break; }
一个 try 语句块抛出多个异常,后面接多个 catch 子句进行处理
标准异常:<stdexcept>定义的异常类
exception 最常见的问题 runtime_error 只有在运行时才能检测出的问题 range_error 运行时错误:结果超出了有意义的值域范围 overflow_error 运行时错误:计算上溢 underflow_error 运行时错误:计算下溢 logic_error 程序逻辑错误 domain_error 逻辑错误:参数对应的结果值不存在 invaild_argument 逻辑错误:无效参数 length_error 逻辑错误:试图创建一个超出该类型最大长度的对象 out_of_range 逻辑错误:使用一个超出有效范围的值