第三章字符串、向量和数组
94页3.3.3节(vector对象操作)练习
#include <iostream>
#include <vector>
#include <string>
//得到对字符操作的标准库
#include<cctype>
using namespace std;
int main() {
//练习一:判断下列vector对象的长度
vector<int>v1; //0
vector<int>v2(10); //10
vector<int>v3(10,42); //10
vector<int>v4{ 10 };//1
vector<int>v5{ 10,42 };//2
vector<string>v6{ 10 };//10 10个空字符
vector<string>v7{ 10,"hi"};//10 10个"hi"
cout << v1.size() << " " << v2.size() << " " << v3.size() << " " << v4.size() << " " << v5.size() << " " << v6.size() << " "
<< v7.size() << endl;
//练习二:读入一组词语并将其转为大写并每行输出
vector<string> v8;
string word;
while (cin >> word) {
if(word!="")
v8.push_back(word);
}
for (auto &i : v8) {
for (auto& j : i) {
j = toupper(j);
}
cout << i << endl;
}
//练习5:读入一组整数并存入vector对象,将每对相邻整数的和输出出来
//练习6:读入一组整数并存入vector对象,先输出第一个和最后一个之和,再输出第二个和倒数第二个之和
vector<int> v9;
vector<int>result;
vector<int>result2;
int number;
while (cin >> number) {
v9.push_back(number);
}
//练习6:
for (decltype(v9.size()) i = 0, j = v9.size() - 1; i < v9.size()/2; ++i) {
result2.push_back(v9[i] + v9[j - i]);
}
for (decltype(v9.size()) i = 0,j=v9.size()-1; i < v9.size()-1; ++i) {
result.push_back(v9[i] + v9[i + 1]);
}
for (auto& i : result) {
cout << i << " ";
}
cout << endl;
for (auto& i : result2) {
cout << i << " ";
}
cout << endl;
return 0;
}
99页3.4.1节(迭代器基本操作)练习
#include <iostream>
#include <vector>
#include <string>
//得到对字符操作的标准库
#include<cctype>
using namespace std;
int main() {
//练习1:使用迭代器将string s="some string"改为全大写
string s = "some string";
auto it = s.begin();
while (it!= s.end()) {
//迭代器取到的是当前元素的指针,想要对元素的值进行读写需要解指针操作
*it = toupper(*it);
++it;
}
cout << s << endl;
//练习2:编写一段程序,创建一个含有十个整数的vector对象,然后使用迭代器将所有元素的值变为原来的两倍,并输出vector对象的内容
vector<int> v;
int j = 0;
for (unsigned i = 0; i < 10; ++i) {
cin >> j;
v.push_back(j);
}
auto it2 = v.begin();
while (it2!=v.end())
{
*it2 = 2 * (*it2);
++it2;
}
for (auto i : v) {
cout << i << " ";
}
cout << endl;
return 0;
}
101页3.4.2节(迭代器运算)练习
#include <iostream>
#include <vector>
#include <string>
//得到对字符操作的标准库
#include<cctype>
using namespace std;
int main() {
//使用迭代器实现二分查找
//text必须是有序的
vector<int> text = { 0,1,2,3,4,5,6,7,8,9,10 };
int x = 0;
//得到首元素迭代器和尾后迭代器
auto beg = text.begin(),end=text.end(),target= text.begin();
//不是(end+beg)/2是为了防止整数相加之后溢出
auto mid = beg + (end - beg) / 2;
string s = "";
cin >> x;
while (mid != end&&*mid!=x) {
if (x>*mid)
{
beg = mid+1;
}
else {
end = mid;
}
mid = beg + (end - beg) / 2;
}
if (beg == end)
{
s = "未找到该数";
}
else {
//查找到的数对应的索引
auto i = mid - target;
cout << i;
s = "找到该数了";
}
cout << s << endl;
return 0;
}
103页3.5.1节(数组定义)练习
#include <iostream>
#include <vector>
#include <string>
//得到对字符操作的标准库
#include<cctype>
using namespace std;
string sa[10];
int ia[10];
//定义数组
int main() {
//初始化数组的维度需要是一个常量表达式即常量
const unsigned sz = 3;
//数组初始化可以不定义维度而直接使用花括号进行具体定义
int a[] = { 1,2,3 };
//也可使用花括号定义部分 b={1,0,0} 不可超过限定的维度 int c[sz] = { 1,2,3,4 };报错
int b[sz] = { 1 };
//ia3中的常量表达式可以在编译时计算出来因此正确,而ia4的txt_size()值在编译时未知因此不可定义
int ia3[4 * 7 - 14];
//int ia4[txt_size()];
//定义字符数组
char c1[] = { 'a','b','c' };
//也可部分定义c2{'a','b','' }
char c2[sz] = { 'a','b'};
//使用字符串字面量对字符数组进行赋值时需要注意字符串后面带有的隐性'\0'也算一个维度,char c3[6] = "Daniel";报错
char c3[7] = "Daniel";
//注意数组不能赋值和初始化另一个数组如 (int d[]={0,1,2} int e[]=d)是错误的
//因为引用不是对象不可定义引用数组
//复杂数组定义
int *ptrs[10]; //定义了含有10个整型指针的数组
int arr[10];
int(*Parry)[10] = &arr; //定义的Parry是一个指向含有10个整型数组的指针
int(&arrRef)[10] = arr;//定义的arrRef是一个含有10个整型数组的引用
//针对上述两个要先从内往外看再从右往左看,先看括号内的内容,例如
int *(&arry)[10] = ptrs; //从内看先是一个引用,再从右往左看是一个含有10个整型指针的数组,因此arry一个含有10个整型指针的数组的引用
string sa2[10];
int ia2[10];
return 0;
}
104页3.5.2节(数组元素访问)练习
#include <iostream>
#include <vector>
#include <string>
//得到对字符操作的标准库
#include<cctype>
using namespace std;
/**/
//数组元素访问
int main() {
//数组下标类型通常使用size_t
//练习1:第一个含有10个int的数组,其值就是其下标
int sz[10];
int sz2[10];
vector<int> v;
for (size_t i = 0; i < 10; i++)
{
sz[i] = i;
cout << sz[i] << " ";
}
cout << endl;
//练习2:将该数组利用vector拷贝给另外一个数组
for (auto i : sz) {
v.push_back(i);
}
for (size_t i = 0; i < 10; i++)
{
sz2[i] = v[i];
cout << sz2[i] << " ";
}
cout << endl;
return 0;
}
108页3.5.3节(指针与数组及迭代器)练习
#include <iostream>
#include <vector>
#include <string>
//得到对字符操作的标准库
#include<cctype>
using namespace std;
/**/
//指针与数组以及迭代器
int main() {
const char* cp="hello"//字符串字面量的类型在c++中时字符数组常量,其中最后隐含了'\0'
//数组赋值给指针则指针默认只想数组的第一个元素的地址
string nums[] = { "one","two","three" };
string* p1 = &nums[0]; //p1指针指向nums的第一个元素
string* p2 = nums;//p1==p2,p2同样指向nums的第一个元素
auto p3(nums);//p3的类型即是string类型的指针
decltype(nums) nums2 = { "1","2","3" }; //可以通过decltype获得数组的类型
//指针也是迭代器能进行迭代器的一系列操作, p1即是对应的begin()数组首元素迭代器
//若想获得数组的尾后迭代器则可对数组尾后元素进行取地址
string* p4 = &nums[3];
//最好使用begin和end两个标准库函数获取数组的迭代器
int ia[] = { 0,1,2,3,4,5,6,7,8,9 };
int* beg = begin(ia);//数组的首元素指针
int* last = end(ia);//数组的尾后元素指针
//指针可以如迭代器一般进行加减计算得到索引差距,类型为ptrdiff_t和迭代的difference_type一样有正负号
string* p5 = nums;
string* p6 = nums;
p5 += p6 - p5;
cout << *p5;
return 0;
}
116页3.6节(多维数组)练习
#include <iostream>
#include <vector>
#include <string>
//得到对字符操作的标准库
#include<cctype>
using namespace std;
/**/
//二维数组
int main() {
//二维数组定义
//二维数组是数组的数组
int ia[3][4];
size_t cnt = 0;
//若使用for(:)对二维数组进行写操作则两层的元素均使用引用
for (auto& row : ia) {
for (auto& col : row) {
col = cnt;
++cnt;
}
}
//若仅对元素进行读操作则除最内层外均需使用引用
for (auto& row : ia) {
for (auto col : row) {
cout << col << " ";
}
}
cout << endl;
//二维数组同样可以使用指针
int(*p)[4] = ia;//指向ia第一行含有4个整数数组的指针
p = &ia[2];//只想ia最后一行含有4个整数数组的指针
//const pstring cstr = 0;和const char *cstr = 0;的区别
//其中pstring定义的是一个指向字符的常量指针,const针对的是该指针,即指针所指的地址不能改变但是能够使用解指针改变对应的字符的值
//而 const char *cstr = 0;中的const则针对字符,即指针所指的地址能够改变但是其值不能改变
typedef char *pstring;
const pstring cstr = 0;
const char *cstr = 0;
return 0;
}
难点理解(类型别名)
//const pstring cstr = 0;和const char *cstr = 0;的区别
//其中pstring定义的是一个指向字符的常量指针,const针对的是该cstr指针,即指针所指的地址不能改变但是能够使用解指针改变对应的字符的值
//而 const char *cstr = 0;中的const则针对字符,即指针所指的地址能够改变但是其值不能改变
typedef char *pstring;
const pstring cstr = 0;
const char *cstr = 0;
第四章表达式
运算符分为一元运算符、二元运算符和三元运算符。作用于一个运算对象的是一元运算符如取地址(&)或解引用(*)。作用于两个运算对象的是二元运算符如相等运算符(==),除此之外还有作用于三个运算对象的三元运算符。
运算过程中运算对象往往会转换为一个类型
左值和右值:在C++中在需要右值的地方可以用左值代替,但不能把右值当成左值使用。当一个左值被当成右值使用时,实际使用的是它的内容(值)。
在C++中,左值是指可以取地址的表达式,右值是指不能取地址的表达式。左值通常代表可修改的对象,而右值通常代表不可修改的常量或临时对象。例如:
变量、数组元素、结构体成员等都是左值。
字面值、字符串字面值、表达式结果、匿名对象、后置自增和自减表达式、lambda表达式等都是右值。
需要注意的是:左值和右值的概念是相对的,同一个表达式在不同的上下文中可能是左值也可能是右值。例如:
在赋值语句中,左侧操作数通常是左值,右侧操作数通常是右值。
在函数调用中,函数参数可以是左值也可以是右值,具体取决于参数类型和传递方式。
123页4.1.2节(复合表达式)练习
#include <iostream>
#include <vector>
#include <string>
//得到对字符操作的标准库
#include<cctype>
using namespace std;
/**/
//复合表达式
int main() {
int result1 = 5 + 10 * 20 / 2;//值为105
//对*s.begin()和*s.begin() + 1两个式子添加括号,使得添加括号后运算对象的组合顺序与添加括号前一致
string s = "result";
cout <<result1<< endl;
cout << *s.begin() << endl;
cout << *(s.begin()) << endl;
auto v = *s.begin() + 1;
auto v2 = (*s.begin()) + 1;
cout << v << endl;
cout << v2 << endl;
return 0;
}
第一节运算符
1.算术运算符
| 运算符 | 功能 |
|---|---|
| + | 一元正号 |
| - | 一元负号 |
| – | – |
| * | 乘法 |
| / | 除法 |
| % | 求余 |
| – | – |
| + | 加法 |
| - | 减法 |
以上算术运算符优先级由高到到低,分为三组每组内优先级相等
注意C++的算术运算溢出和C类似当最大值和最小值是呈环形存储如:
short a=32767;
short value=1;
a=a+1;//此时a的最大值加一则溢出变为最小值,a=-32768
注意:
//(-m)/n=m/(-n)=-(m/n)
//m%(-n)=m%n
//(-m)%n=-(m%n)
int a=-21/8;//a=-2
int b=21%-5;//b=1
int c=-21%5;//c=-1
2.逻辑和关系运算符
| 结合律 | 运算符 | 功能 |
|---|---|---|
| 右 | ! | 逻辑非 |
| – | – | – |
| 左 | < | 小于 |
| 左 | <= | 小于等于 |
| 左 | > | 大于 |
| 左 | >= | 大于等于 |
| – | – | – |
| 左 | == | 相等 |
| 左 | != | 不相等 |
| – | – | – |
| 左 | && | 逻辑与 |
| – | – | – |
| 左 | | | | 逻辑或 |
逻辑与和逻辑或均会采用短路求值,关系运算符比较后的返回值为布尔值因此不可使用a<b<c进行判断
#include <iostream>
#include <vector>
#include <string>
//得到对字符操作的标准库
#include<cctype>
using namespace std;
/**/
int main() {
//指针指向字符串或字符数组时直接输出指针则会输出整个字符串(包括\0)或字符数组
//字符数组需要添加'\0'
const char* cp = "Hello World";
const int a = 1;
const int* p = &a;
cout << p << " " << *p << endl;
cout << cp << " " << *cp << endl;//"Hello World" "H"
return 0;
}
3.赋值运算符=
左值=右值
往往右值会转换为左值的类型,若右值类型的范围大于左值则可能会损失一定精度造成窄化转换
赋值运算符的优先级较低因此要先赋值时要使用括号
复合赋值运算符
| 算术运算 | 位运算运算 |
|---|---|
| += | <<= |
| -= | >>= |
| *= | &= |
| /= | ^= |
| %= | |= |
int i;
double d;
d = i = 3.5;
cout << i << " " << d << endl; //i=3 d=3
i = d = 3.5;
cout << i << " " << d << endl; //i=3 d=3.5
4.递增和递减运算符
递增运算符(++)和递减运算符(–)分为前置(++i)和后置(i++)两种形式。
前置版本:先将运算对象+1,然后将改变后的对象作为求值结果。
后置版本:运算对象+1,然后会将改变前的运算对象作为求值结果。
int j = 0, k;
k = ++j;//k=1,j=1 前置版本得到递增后的值
k = j++;//k=1,j=2 后置版本得到递增前的值
尽量使用前置版本,因为后置版本为了保存递增前的值浪费了一定资源
解引用和递增运算符混合使用
auto a=*pbeg++//整个相当于*(pbeg++),即将pebg向前移动一步,并将移动前的指针进行解指针,并将值赋给a
如果一个表达式的子表达式改变了某个运算对象的值,另一条子表达式又要使用该值的话可能会导致一些问题如:
while (beg != s.end() && !isspace(*beg)) {
*beg = toupper(*beg++);//错误,赋值运算两端都使用了beg,并且右侧改变了beg的值所以该赋值语句未定义
}
5.成员访问运算符
即点运算符和箭头运算符,表达式
p->size()等价于 (*p).size()
注意:点运算符优先级高于解指针运算符(*)
6.条件运算符
cond?expr1:expr2;
条件运算符的优先级低于算术运算符因此
string s2 = "word";
string p1 = s2 + s2[s2.size() - 1] == 's' ? "" : "s";//无法编译需改为下列形式
string p1 = s2 + (s2[s2.size() - 1] == 's' ? "" : "s");
7.位运算符
位运算符作用于整数类型的运算对象,将运算对象看成二进制位的集合
位运算符
| 运算符 | 功能 | 用法 |
|---|---|---|
| ~ | 位求反 | ~expr |
| << | 左移 | expr1<<expr2 |
| >> | 右移 | expr1>>expr2 |
| & | 位与 | expr&expr |
| ^ | 位异或 | expr^expr |
| | | 位或 | expr|expr |
移位运算<<或>> 则将左侧运算对象按右侧对象要求移动指定位数并拷贝左侧对象作为结果,右侧对象不能为负并且其值要严格小于结果的位数。左移或右移超出边界的位数被舍弃
//假设char占8位 int占32位
unsigned char bits=0233;//8进制 转为2进制为 10011011
bits<<8 //左移八位 bits提升成int类型然后向左移动8位变为
//00000000 00000000 10011011 00000000
bits<<31 //左移31位超出边界的位丢弃变为
//10000000 00000000 00000000 00000000
bits>>3 //右移3位最右边变为
//00000000 00000000 00000000 00010011
注意:左移固定添加0,右移则要根据运算对象,若运算对象是无符号数则添加0,如果是带符号数插入其符号位的副本
位求反运算符(~) 将对象逐位求反即1变0、0变1
//假设char占8位 int占32位
unsigned char bits=0233;//8进制 转为2进制为 10011011
~bits //bits提升成int类型取反变为
//11111111 11111111 11111111 01100100
位与、位或、位异或运算符
//假设char占8位 int占32位
unsigned char b1=0145;//2进制为 01100101
unsigned char b2=0257;// 10101111
b1&b2 //位与运算全1为1不同为0得到 00100101
b1|b2 //位或运算有1则1全0为0得到 11101111
b1^b2//位异或运算不同为1相同为0 11001010
//11111111 11111111 11111111 01100100
位运算符优先级比算术运算符低,高于关联运算符、赋值运算符和条件运算符
8.sizeof运算符
sizeof运算符返回一条表达式或者一个类型名所占的字节数,所得的值位size_t类型,满足的是右结合律
sizeof(type)
size expr
//能够根据该运算符得到数组的元素数量,ia为数组
constexpr size_t sz=sizeof(ia)/sizeof(*ia);
int arr[sz];
//因为sizeof(指针)表示的是指针地址的大小在64位系统中即为8个字节
int x[10];
int *p3 = x;
cout << sizeof(x) << " " << sizeof(*x) << endl;//40 4
cout << sizeof(p3) << " " << sizeof(*p3) << endl;//8 4
9.逗号运算符
含有两个运算对象,按照从左往右顺序依次求值,常用于for循环中
第一节类型转换
1.隐式转换
一般以下情况会发生隐式类型转换:
(1)在大多数表达式中,比int类型小的整型值会先提升为较大的整型类型
(2)在条件中非布尔类型会转换为布尔类型
(3)初始化过程中,初始值转换成变量的类型;赋值语句中,右侧运算对象转换成左侧运算对象类型
(4)如果算术运算或关系运算的对象有多种类型,需要转换成同一种类型
(5)函数调用时也会发生类型转换
2.算术转换
如果运算对象要考虑是否带符号的则:运算对象都是带符号的或者都是无符号的则小类型的运算对象转换成较大的类型。若一个对象是无符号类型另一个对象是带符号类型的,如果两个类型是匹配的如unsigned int 和 int则带符号转为无符号的,如果带符号类型大于无符号类型则看机器,如果带符号的类型占用空间大于无符号类型则无符号转为带符号,若相等则转为无符号类型
#include <iostream>
#include <vector>
#include <string>
//得到对字符操作的标准库
#include<cctype>
using namespace std;
/**/
//算术转换
int main() {
bool flag;
char cval;
short sval;
unsigned short usval;
int ival;
unsigned int uival;
long lval;
unsigned long ulval;
float fval;
double dval;
3.14159L + 'a';//'a'提升为int之后转换成long double
dval + ival;//ival转为double类型
dval + fval;//fval转为double类型
ival = dval;//dval转为int类型(切除小数部分)
flag = dval;//如果dval是0则flag为false否则为true
cval + fval;//cval提升为int然后int转为float
sval + cval;//sval和cval都提升为int
cval + lval;//cval转换成long
ival + uival;//ival转换成 unsigned int
usval + ival;//根据unsigned short和int类型所占空间大小进行提升,小的提升为大的
uival + lval;//根据unsigned int和long类型所占空间大小进行提升,小的提升为大的
return 0;
}
3.其他隐式转换
(1)数组转为指针
当数组被用作decltype()的参数或者作为取地址符(&)、sizeof及typeid等运算符的运算对象时转换不会发生
int ia[10];
int *p=ia;
(2)指针的转换
0或者nullptr能转换成任意类型的指针
指向任意非常量的指针能转换成void*
指向任意对象的指针能转换成const void*
(3)转换成布尔类型
如果算术类型或者指针类型的值为0或nullptr则为false否则转换结果为true
(4)转换成常量
允许指向非常量类型的指针或引用转换成指向常量类型的指针或引用
int i;
const int &j=i;
const int *p=&i;
int &r=j,*q=p;//错误,常量不可转为非常量
(5)类类型定义的转换
string s,t="a value";//字符串字面量转换成string类型
while(cin>>s)//cin转换成布尔类型
4.显示转换
将对象强制转换成另一种类型
(1)命名的强制类型转换,格式如下:
cast-name(expression);
type 为转换的目标类型而expression为要转换的值,type如果是引用类型则结果是左值。
cast-name分别为static_cast、dynamic_cast、const_cast和reinterpret_cast的一种
static_cast:任何有明确定义的类型转换只要不包含底层const均可使用,对于强制将大类型转为小类型非常有用,对于无法自动类型转换的类型转换也非常有用
double slope = static_cast<double>(j) / i;
void* p = &d;
double* dp = static_cast<double*>(p);//将void类型指针转为double类型指针,但要确保指针的值转换前未发生改变
const_cast
只能改变运算对象的底层const(注const分为高层和底层const,指针本身是常量则为顶层const,指针不是常量但是指向常量则为底层const)该操作称为(去const性质)
const char *pc;
char *p=const_cast<char*>(pc)//正确,但是如果pc指向的是一个常量则不能进行修改反之则可
string q=const_cast<string>cp//错误只改变常量属性
reinterpret_cast
通常为运算对象的位模式提供较低层次上的重新解释,reinterpret_cast 是 C++ 中最强大和最危险的类型转换操作符之一。它可以在不同类型之间进行非标准的转换,通常用于处理底层的数据表示,如指针或者整数类型之间的转换。
int *ip;
char *pc=reinterpret_cast<char*>(ip);//pc所指的真实对象是一个int而非char
string str(pc)//这样会出现问题
(1)旧式的强制类型转换,格式如下:
type(expr);//函数形式
(type)expr;//C语言风格
运算符优先级表


第五章语句
1.空语句
直接使用一个分号(;)代表,多余地使用空语句可能会出现一定的问题。
2.复合语句(块)
指用花括号括起来的语句和声明的序列,一个块就是一个作用域,块内变量只能在块内以及子块中使用。
3.语句作用域
在块中定义的变量,当语句结束后,变量就超出范围了不可使用了。
while (int i=get_num())//每次迭代创建并初始化i
cout<<i<<endl;
i=0;//错误在循环外部无法访问i
4.条件语句
跟条件判断执行的语句共有两种分别是if和switch,语法形式分别为:
if的语法形式是:
//最好使用花括号控制if的路径
if(condition){
statement;
}else if(condition){
statement;
}else{ //如果有多个else和if,则else和最近的尚未匹配的if匹配
statement;
}
switch的语法形式是:
#include <iostream>
#include <vector>
#include <string>
//得到对字符操作的标准库
#include<cctype>
using namespace std;
/**/
//使用switch统计五个元音字母在文本中出现的次数
int main() {
//用于统计
unsigned aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0;
char ch;
while (cin >> ch) {
//先对括号里的表达式求值,将值转换成整数类型,然后与每个case标签的值比较
//如果表达式和某个case标签的值匹配成功,
//程序就从该标签之后的第一条语句开始执行直到达到了switch的结尾或者遇到一条break语句为止
//case标签必须是整型常量表达式
switch (ch)
{
case 'a':
++aCnt;
break;
case 'e':
++eCnt;
break;
case 'i':
++iCnt;
break;
case 'o':
++oCnt;
break;
case 'u':
++uCnt;
break;
default:
break;
}
}
cout << "Number of vowel a: \t" << aCnt << '\n'
<< "Number of vowel e: \t" << eCnt << '\n'
<< "Number of vowel i: \t" << iCnt << '\n'
<< "Number of vowel o: \t" << oCnt << '\n'
<< "Number of vowel u: \t" << uCnt << endl;
return 0;
}
//该段代码出现错误,因为case标签后要是整型常量因此将下列改为
//const unsigned ival = 512, javl = 1024, kval = 4096;
unsigned ival = 512, javl = 1024, kval = 4096;
unsigned bufSize;
unsigned swt = get_bufCnt();
switch (swt)
{
case ival:
bufSize = ival * sizeof(int);
break;
case javl:
bufSize = javl * sizeof(int);
break;
case kval:
bufSize = javl * sizeof(int);
break;
default:
break;
}
//该段代码出现错误,原因在于case1和defult共享同一个作用域,在case1中已经声明并初始化了ix
//因此default中再使用ix,由于是同一个作用域使用不需要再次声明会导致变量的值可能会在 case 分支之间传递
unsigned index =some_value();
switch (index)
{
case 1:
int ix = get_value();
ivec[ix] = index;
break;
default:
ix = ivec.size() - 1;
ivec[ix] = index;
}
//修改后
unsigned index = some_value();
switch (index)
{
case 1:
{
int ix = get_value(); // 在 case 1 分支内部重新声明 ix
ivec[ix] = index;
break;
}
default:
{
int ix = ivec.size() - 1; // 在 default 分支内部重新声明 ix
ivec[ix] = index;
}
}
5.迭代语句
while语句
只要条件为真,while语句就重复地执行循环体,其语法形式为:
while(condition){
statement;
}
会先进行条件判断,若初始条件就为false则不进入循环。当不确定要迭代多少次时,使用while循环比较合适。
传统for语句
for循环适用于确定迭代次数的情况,其语法形式为:
for (init-statement;condition;expression){
statement;
}
其中for的语句头中定义的对象只在循环体中使用,可在init-statement定义多个对象,但只能有一个声明语句因此对象类型必须相同例如:
for(decltype(v.size())i=0,sz=v.size();i!=sze;++i){
v.push_back(v[i]);
}
也可以省略for语句头的某些部分,能省略掉三个部分中的任意一个或全部。
auto beg=v.begin();
for(;beg!=v.end()&&*beg>=0;++beg){//能省略但是要保留分号
;//空语句什么也不做
}
范围for语句
能够直接遍历容器或其他序列的所有元素,其语法形式为:
for(declaration:expression){
statement;
}
其中expression必须为一个序列比如用花括号括起来的初始值列表、数组、vector对象或者string对象,均可以返回迭代器的begin和end成员
declaration则是定义一个遍历最简单的是使用auto类型,若要对序列元素进行写操作必须声明称引用类型
注意:不能通过范围for添加或删除vector对象(或其他容器)的元素,因为范围for循环预存了end()的值
vector<int> v={0,1,2,3,4,5,6,7,8,9};
for(auto &r:v){
r*=2;
}
//与之等价的传统for循环
for(auto beg=v.begin(),end=v.end();beg!=end;++beg){
auto &r=*beg;
r*=2;
}
do-while语句
do-while语句和while语句相似,唯一的区别在于do-while是先进入循环体一次再进行条件判断,不管条件的值如何至少执行一次循环。语法形式如下:
do{
statement;
}while(condition);
不允许在do-while语句的条件部分定义变量
do{
mumble(foo);
}while(int foo=get_foo()) //错误,将变量声明放在了do的条件部分
6.跳转语句
跳转语句包含break、continue、goto、return
break:负责终止离它最近的while、do-while、for或者switch语句
continue:终止最近的循环体中的当此迭代并立刻开始下一次迭代,只出现在for、while和do-while循环的内部
goto:goto语句的作用是从goto语句无条件跳转到同一个函数内的另一个语句,尽量不要使用
其语法形式是:
//goto label;其中label是用于表示一条一句的标识符
//使用标签名加冒号定义标签
int main() {
int i = 0;
start: // 定义一个标记
cout << i << endl;
i++;
if (i < 5)
goto start; // 转移到标记处
return 0;
}
7.try语句块和异常处理
对于程序中可能引发异常的代码要有相应的代码进行处理,即异常处理机制
异常处理包括:
throw 表达式(throw expression):
异常检测部分使用throw表达式来表示它遇到了无法处理的问题。即throw引发了(raise)异常
try语句块(try block):
异常处理部分使用try语句块处理异常。try语句块以关键字try开始并以一个或多个catch子句结束。try语句块抛出的异常会被某个catch子句处理,因此也称catch子句为异常处理代码。
//try语句块的通用语法形式是
try{
program-statements;
}catch(exception-declaration){
handler-statements;
}
Sales_item item1,itme2;
while(cin>>item1>>item2){ //输入两本书的销量信息
try{
item1+item2;//判断这两本的编号,若编号不一致则抛出异常
//若相加失败会抛出runtime_error
}catch(runtime_error err){
cout<<err.what()<<"\nTry Again? Enter y or n"<<endl;
char c;
cin>>c;
if(!cin||c=='n')
break;
}
}
异常类:
用于在throw表达式和相关的catch子句之间传递异常的具体信息
Sales_item item1,itme2;
cin>>item1>>item2;//输入两本书的销量信息
//判断这两本的编号,若编号不一致则抛出异常
if(item1.isbn()!=item2.isbn()){
throw runtime_error("Data must refer to same ISBN");
cout<<item1+item2<<endl;
//结合使用
int i, j,k;
while (cin >> i >> j) {
try {
if (i != j) {
throw runtime_error("Data must same");
}
else {
k = i + j;
cout << "相加成功值为\t" << k << endl;
}
}
catch(runtime_error err){//err.what()返回的是const char* 类型的指针
cout << err.what() << "\nTry Again? Enter y or n" << endl;
char c;
cin >> c;
if (!cin || c == 'n')
break;
}
}
注意: 在复杂代码中一个try语句块可能调用了包含另一个try语句块的函数,当异常被抛出时,先搜索抛出该异常的函数如果没有找到匹配的catch子句,则终止该函数,并在调用该函数的函数中继续寻找,以此类推直到找到catch为止。
标准异常:
异常类分别定义在四个头文件中分别为:
excepttion头文件: 定义了最通用的异常类exception,只报告了异常的发生不提供任何额外信息
stdexcept头文件: 定义了几种常用的异常类
| excpetion | 最常见的问题 |
| runtime_error | 只有在运行时才能检测出的问题 |
| range_errot | 运行时错误:生成的结果超出了有意义的值域范围 |
| overflow_error | 运行时错误:计算上溢 |
| underflow_error | 运行时错误:计算下溢 |
| logic_error | 程序逻辑错误 |
| domain_error | 逻辑错误:参数对应的结果值不存在 |
| invalid_argument | 逻辑错误:无效参数 |
| length_error | 逻辑错误:试图创建一个超出该类型最大长度的对象 |
| out_of_range | 逻辑错误:使用一个超出有效范围的值 |
new头文件: 定义了bad_alloc异常类型
type_info头文件: 定义了bad_cast异常类型
第六章函数
1.函数基础
函数定义包括:返回类型、函数名、0个或多个形参以及函数体。
函数的调用包括:函数名加圆括号(),圆括号中为用逗号隔开的实参列表
形参列表可以是空的或者使用void表示空,且每个形参必须有一个声明符,且形参不能同名。
形参和函数体内部定义的变量统称为局部变量,仅在函数的作用域内可见,局部变量的声明周期由定义方式决定,分为自动对象和局部静态对象。
自动对象:挡块执行到定义语句时定义,块执行结束后销毁
局部静态对象:在变量定义语句的类型前添加static获得局部静态对象。在程序执行第一次经过定义语句时初始化,并到程序终止时销毁,此期间即是所在函数执行结束也不会受到影响。
函数声明(又叫函数原型):描述了函数的接口,无需花括号表示函数体,仅用分号结尾即可
2.参数传递
每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。
如果形参时引用类型则将形参绑定到对应实参上;否则将实参的值拷贝赋给形参。
2.1传值,传指针,传引用
//函数声明
void fact1(int val);
void fact2(int *val);
void fact3(int &val);
#include"Chapter6.h"
void fact1(int val) {
int ret = 1;//局部变量
if (val > 1)
val--;
}
void fact2(int *val) {
int ret = 1;//局部变量
if (*val > 1)
--*val;
}
void fact3(int &val) {
int ret = 1;//局部变量
if (val > 1)
++val;
}
#include <iostream>
#include <vector>
#include <string>
//得到对字符操作的标准库
#include<cctype>
#include<cstring>
#include "Chapter6.h"
using namespace std;
//传值,传指针,传引用
int main() {
int n1 = 2;
int n2 = 2;
int n3 = 2;
fact1(n1);//传值参数,n1不发生改变2
fact2(&n2);//传指针,n2改变为1
fact3(n3);//传引用,n3变为3,对于大的类类型或容器对象可以传引用,还可以利用传引用返回额外信息,C++中通常使用传引用
cout << n1 << "\n" << n2 << "\n" << n3;
}
2.2const的形参和实参
当形参为顶层const时,使用常量对象或非常量对象均能对其初始化,会忽略其顶层const。
//会发生错误因为忽略了顶层const,实际上两个函数是一致的因此重复定义会有问题
void fact1(int val) {
int ret = 1;//局部变量
if (val > 1)
val--;
}
void fact1(const int val) {
int ret = 1;//局部变量
if (val > 1)
val--;
}
形参初始化和变量初始化方式一致,能够使用非常量初始化一个底层const对象但是反过来不行
int i=0;
const int ci=i;
string::size_type ctr=0;
reset(&i);//调用形参类型是int*的reset函数
reset(&ci);//错误,不能用const int对象的指针初始化int*
reset(i);//调用形参类型是int&的reset函数
reset(ci);//错误,不能把普通引用绑定到const对象的ci上
reset(42);//错误,不能把普通引用绑定到字面量上
reset(ctr);//错误,类型不匹配,ctr是无符号类型
注意:形参定义引用尽量使用常量引用
2.3数组形参
数组不可进行拷贝,因此为函数传递一个数组时实际上传递的是数组的首元素指针。
//三种等价的数组形参定义,每个函数都有一个const int*类型的形参
void print(const int*);
void print(const int[]);
void print(const int[10]);
2.4管理指针形参
由于数组以指针形式传递给函数,因此最好提供一定额外信息防止出现问题,管理指针形参有三种常用技术:
使用标记指定数组长度
//适用于有明显结束标记的数据
void print(const char *cp){
if(cp){
while(*cp){
count<<*cp++<<endl;
}
}
}
使用标准库规范
//使用标准库函数得到首元素和尾后指针
void print(const int *beg,const int *end){
while(beg!=end){
count<<*beg++<<endl;
}
}
int j[2]={0,1};
print(begin(j),end(j));
显示传递一个表示数组大小的形参
void print(const int ia[],size_t size){
for(size_t i=0;i!=size;++i){
count<<ia[i]<<endl;
}
}
int j[2]={0,1};
print(j,end(j)-begin(j));
当要对数组进行写操作是才使用非常量指针
2.5数组引用形参
形参可以是数组的引用,将形参绑定到对应的实参上,但这样无形中限制了函数的使用性,因为规定了数组的大小。
void print(int (&arr)[10]){
for(auto elem:arr){
count<<elem<<endl;
}
}
2.6传递多维数组
void print(int (*matrix)[10],int rowSize){}
//等价定义
void print(int matrix[][10],int rowSize){}
2.7main函数处理命令行选项
int main(){}//假如main函数位于prog文件当中
//可以根据以下向main传递实参
prog -d -o ofile data0
//上述命令行通过两个形参传递给main
int main(int argc,char *argv[]){}
//argc表示argv数组中的字符串数
//argv为一个指向字符串的指针数组
argv[0]="prog";//固定指向程序名或者空字符串
argv[1]="-d";//实参从1开始
argv[2]="-o";
argv[3]="ofile";
argv[4]="data0";
argv[5]=0;
2.8含有可变形参的函数
若不确定实参数量但是全部实参的类型都相同可以使用initializer_list类型的形参,但是initializer_list对象中的元素永远是常量值不可进行改变。
#include<initializer_list>
//initializer_list类型和vector类型相似也有begin()、end()、size()
//initializer_list<T> lst;初始化
//lst2(lst) lst2=lst,拷贝或赋值一个initializer_list对象,拷贝后两者共享元素
void error_msg(initializer_list<string> il){
for(auto beg=il.begin();beg!=il.end();++beg){
cout<<*beg<<endl;
}
}
string excepted="",actual="";
error_msg({"functionX",excepted,actual});
2.9省略符形参
省略符形参是为了C++程序访问使用了名为varargs标准库功能的特殊C代码而设置的,不应用于其他目的
//只用两种形式
void foo(param_list,...);//该形式制定了部分形参的类型,该部分会进行类型检查,省略号对应的实参不进行检查
void foo(...);
3.返回类型和return语句
return语句表示终止当前正在执行的函数并将控制权返回到调用该函数的地方。有两种形式:
return;
return expression;
3.1无返回值函数
没有返回值的return只能用在类型为void的函数中,返回void的函数中不一定非要有return,往往会隐式执行return。
注意:void函数也可返回具体值,如果是返回的另一个void类型函数则可强制返回
3.1有返回值函数
只要函数返回值类型不是void的必须return一个值,返回值的类型和函数类型一致。
3.2值是如何返回的
返回一个值的方式和初始化一个变量或形参的方式完全一样,返回值用于初始化调用点的一个临时量,这个临时量就是函数调用的结果。要注意函数返回局部变量时的初始化规则,不要返回局部对象的引用或指针。
3.3引用返回左值
函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到左值,其他返回类型得到右值。因此能够使用返回引用的函数的调用如:
char &get_value(string &str,string::size_type ix){
return str[ix];
}
int main(){
string s("a value");
cout<<s<<endl;
get_value(s,0)='A'//将s[0]改为'A'
cout<<s<<endl;//输出A value
return 0;
}
shorterString("hi","bye")="X"//错误,返回值为一个常量
3.4列表初始化返回值
C++11规定,函数可以返回花括号包围的值的列表。如果函数返回的是内置类型,则花括号包围的列表最多包含一个值。
vector<string> process(){
string s;
return{};//返回空表示返回一个空的vector对象
return{"functionX","okey"}//返回列表初始化的vector对象
return{"functionX",s}
3.5主函数main的返回值
可以使用cstdlib头文件的两个预处理变量分别表示main函数的成功与失败
int main(){
if(some_failure){
return EXIT_FAILURE;
}else{
return EXIT_SUCCESS;
}
}
3.6递归
函数调用了其自身就成为递归
//递归实现求阶乘
int factorial(int val){
if(val>1){
return factorial(val-1)*val;
return 1;
}
3.7返回数组指针
因为数组不能拷贝所以函数不能返回数组但是可以返回数组的指针。最直接的方法就是使用类型别名
typedef int arrT[10];//arrT是一个类型别名,表示含有10个整数的数组
using arrT = int[10];//等价声明
arrT* func(int i);//func返回一个指向含有10个整数的数组的指针
若不适用类型别名要记住,数组的维度跟随在要定义的数组名之后
int arr[10];//定义一个整型数组
int* p1[10];//定义一个含有十个整型指针的数组
int(*p2)[10];//定义一个指针指向含有十个整数的数组
//返回数组指针的函数形式如下:
//Type(*function(parm_list))[dimension]
int (*func(int i))[10]{};
也可以使用尾置返回类型
//尾置返回类型如下,在形参列表后面用->开头,为了表示函数真正的返回类型跟在形参列表之后
auto func(int i)->int(*)[10]{}
使用decltype
如果我们知道函数返回的指针将指向那个数组可以使用decltype关键字声明返回类型
int odd[]={1,3,5,7,9};
int even[]={2,4,6,8,10};
decltype(odd) *arrPtr(int i){
return (i%2)?&odd:&even;
}
4.函数重载
如果同一作用域内的几个函数名字相同但是形参列表不同(形参数量或形参类型不同),称之为函数重载。
4.1判断两个形参的类型是否相异
有时候两个形参列表看起来不一样实际上相同
//相同省略了形参名字
int lookup(const Account &acct);
int lookup(const Account&);
typedef Phone Telno;
int lookup(const Phone&);//类型相同
int lookup(const Telno&);
//顶层const形参和普通形参是一样的,而底层const则不一样
//两者类型相同,重复声明
int lookup(Phone);
int lookup(const Phone);
//两者类型相同,Phone* const 为顶层const 重复声明
int lookup(Phone*);
int lookup(Phone* const );
//两者类型不同,const Phone&为底层const
int lookup(Phone&);
int lookup(const Phone&);
4.2const_cast和重载
使用const_cast进行去底层const操作
const string& shorterString(const string& s1, const string& s2) {
return s1.size() <= s2.size() ? s1 : s2;
}
//若想使用非常量string实参调用shorterString并返回非常量引用则对其进行重载并使用const_cast进行去底层const操作
string& shorterString(string& s1, string& s2) {
auto& r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2));
return const_cast<string&>(r);
}
4.3重载和作用域
string read();
void print(const string&);
void print(double);//重载
void fooBar(int ival) {
bool read = false;//新作用域隐藏了外层的read
string s = read();//错误read是一个布尔值而非函数
void print(int);//新作用域隐藏了之前的print
print("Value: ");//错误void print(const string&);被隐藏
print(ival);//正确 当前print(int)可见
print(3.14);//正确调用print(int),void print(double);被隐藏
}
5.函数重载
5.1默认实参
某些函数有这样一种形参,在函数的很多次调用中它们都被赋予了一个相同的值,此时我们把这个反复出现的值称为函数的默认实参。调用含有默认实参的函数时,可以包含该实参也可以省略该实参。
typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrnd = '');
string window;
window = screen();//等价于screen(24, 80, '')
window = screen(66);//等价于screen(66, 80, '')
window = screen(66,256);//等价于screen(66, 256, '')
window = screen(66,256,'#');//等价于screen(66, 256, '#')
window = screen(,,'');//错误,只能省略尾部的实参
//函数能被多次声明但在给定的作用域中一个形参只能被赋予一次默认实参
//换句话说,函数的后续声明只能为之前没有默认值的函数添加默认实参,
//且该形参右侧的所有形参都必须要有默认值,如给定
string screen(sz,sz,char='');//前两个形参没有默认值
string screen(sz,sz,char='*');//错误不能修改已经存在的默认值
string screen(sz=24,sz=80,char)//默认实参声明
//局部变量不能作为默认实参,除此之外只要表达式类型能转换乘形参所需的类型,该表达式就能作为默认实参
sz wd=80;
char def=' ';
sz ht();
string screen(sz ht(),sz=wd,char=def);
5.2内联函数和constexpr函数
内联函数:由于调用函数一般比求表达式的值要慢一些,为了避免函数调用的开销可以在调用点上”内联地”展开
//内联函数版本的比较两个string对象中较短的那个,在类型前加inline
inline const string& shorterString(const string &s1,const string &s2){
return s1.size()<=s2.size()?s1:s2;
}
cout << shorterString(s1,s2)<<endl;
//使用内联后编译过程会变为
cout << (s1.size()<=s2.size()?s1:s2)<<endl;
内联函数适用于规模小、流程直接、频繁调用的函数
constexpr函数:指能用于常量表达式的函数,和定义其他函数类似,不过要遵循几项约定:
(1)函数的返回类型以及所有的形参类型都是字面量形式
(2)而且函数体中必须有且只有一条return语句
constexpr int new_sz() {
return 42;
}
//允许返回值并非一个常量
constexpr size_t scale(size_t cnt) {
return cnt * new_sz();
}
int arr[scale(2)]; //正确2是常量
int i = 2;
int arr[scale(i)]; //错误i是变量
要把内联函数和constexpr函数放在头文件中
5.3调试帮助
assert预处理宏:一个预处理变量在cassert头文件中,使用一个表达式作为它的条件
assert (expr);
先对expr求值如果表达式为假(0),assert输出信息并终止程序执行,如果为真(1)则什么都不做。
NOEBUG预处理变量:如果定义了NOEBUG则assert什么都不做,默认情况下NOEBUG未定义。可以使用**#include定义NOEBUG关闭assert检查**,也可以使用NOEBUG编写自己的条件调试代码。
//如果未定义NOEBUG则执行#ifndef和#endif间的代码,否则不执行
void print(const int ia[], size_t size) {
#ifndef NOEBUG
cerr << __func__ << ":array size is" << size << endl;
#endif
}
编译器为每个函数都定义了用于程序调试很有用的名字:
__func__:输出当前调试函数的名字
__FILE__:输出存放文件名的字符串字面量
__LINE__:输出存放当前行号的整型字面量
__TIME__:输出存放文件编译时间的字符串字面量
__DATE__:输出存放文件编译日期的字符串字面量
6.函数匹配
当几个重载函数的形参数量相等以及某些形参的类型可以由其他类型转换而来时,确定某次调用该选用哪个重载函数将会变得困难。以下列为例:
void f();
void f(int);
void f(int,int);
void f(double,double=3.14);
f(5.6)//调用f(double,double=3.14);
函数匹配的步骤:
(1)确定候选函数和可行函数: 根据被调用函数名得到4个候选函数,根据调用提供的实参确定可行函数。可行函数有两个特征:一是其形参数量与本次调用提供的实参数量相等;二是每个实参的类型与对应的形参类型相同或者能够转换成形参的类型。
(2)寻找最佳匹配(如果有的话): 实参类型与形参类型越接近,匹配得越好
含有多个形参的函数匹配:
如果f(42,2.56);此时可行函数包括f(int,int);和f(double,double);,接下来依次检查每个实参以确定哪个函数是最佳匹配。如果有且只有一个函数满足下列条件,则匹配成功:
(1)该函数每个实参的匹配都不劣于其他可行函数需要的匹配
(2)至少有一个实参的匹配优于其他可行函数提供的匹配
如果没有一个函数脱颖而出则会报二义性调用的错误。如上述情况。
实参类型转换:编译器将实参类型到形参类型的转换分为以下等级
(1)精确匹配,包括以下情况:
实参类型和形参类型相同。
实参从数组类型或函数类型转换成对应的指针类型。
向实参添加顶层const或者从实参中删除顶层const。
(2)通过const转换实现的匹配
(3)通过类型提升实现的匹配
(4)通过算术类型转换或指针转换实现的匹配
(5)通过类类型转换实现的匹配
7.函数指针
函数指针指向的是函数而非对象。函数指针指向某种特定类型,由函数的返回值和形参类型共同决定。
bool lengthCompare(const string &,const string &);
该函数类型为bool (const string &,const string &)。想声明一个指向该函数的指针只需要用指针替换函数名即可
bool (*pf)(const string &,const string &);//一定要有括号
使用函数指针
把函数名作为一个值使用时,该函数自动转换成指针。
//两者一样
pf=lengthCompare;
pf=&lengthCompare;
此外还可以直接使用函数指针调用该函数,无需提前解指针
//三者一样
bool b1=pf("hello","goodbye");
bool b2=(*pf)("hello","goodbye");
bool b3=lengthCompare("hello","goodbye");
不同函数类型的指针不能转换,但都可以赋给指针一个nullptr或者0
重载函数的指针
使用重载函数的指针必须确定指针类型指向哪个重载函数
void ff(int*);
void ff(unsigned int);
void (*p1)(unsigned int);//正确,p1指向void ff(unsigned int)
void (*p2)(int);//错误,找不到和p2类型匹配的ff
函数指针的形参
函数指针可以用作函数类型的形参
//pf1是函数类型会自动转换成指向函数的指针
void useBigger(const string &s1,const string &s2,
bool pf1(const string&,const string&));
//两者等价
void useBigger(const string &s1,const string &s2,
bool (*pf2)(const string&,const string&));
//传递实参时就可以直接将函数作为实参传入
useBigger(s1,s2,lengthCompare);
//使用别名简化函数指针
//pf1和pf2等价,是函数的类型
typedef bool pf1(const string&,const string&);
typedef decltype(lengthCompare) pf2;
//定义函数指针,pf1p和pf2p等价
typedef bool(*pf1p) (const string&,const string&);
typedef decltype(lengthCompare)* pf2p;
//函数声明中使用别名
void useBigger(const string &s1,const string &s2,
pf1);
//两者等价
void useBigger(const string &s1,const string &s2,
pf2p);
返回函数指针
和数组类似虽然不能返回一个函数但是可以返回函数指针。
(1)使用类型别名
using F=int(int*,int);//F为函数类型不是指针
using PF=int(*)(int*,int);//PF是指针类型
PF f1(int);//正确
F f1(int);//错误,函数不能返回函数类型
F* f1(int);//正确
(2)直接声明
int (*f1(int))(int*,int){}
(3)尾置返回类型的方式
auto f1(int)->int(*)(int*,int){}
(4)使用decltype
int sumLength(int*,int);
decltype(sumLength)* f1(int){}
第七章类
类的基本思想是数据抽象和封装。数据抽象是一种依赖于接口和实现分离的编程。类的接口包括用户所能执行的操作;类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。封装实现了类的接口和实现的分类。类想要实现数据抽象和封装,首先要定义一个抽象数据类型,在其中考虑类的实现过程。
1.定义抽象数据类型
1.1设计Sales_data类
Sales_data的接口应该包含以下操作:
(1)一个isbn成员函数,用于返回对象的ISBN编号
(2)一个combine成员函数,用于将一个Sales_data对象加到另一个对象上
(3)一个名为add的函数,执行两个Sales_data对象的加法
(4)一个read函数,将数据从istream读入到Sales_data对象中
(5)一个print函数,将Sales_data对象的值输出到ostream
1.2定义改进的Sales_data类
struct Sales_data {
//新成员:关于Sales_data对象的操作
//bookNo表示ISBN编号
//units_sold表示某本书的销量
//revenue表示这本书的总销售收入
string isbn() const {
return bookNo;
}
Sales_data& combine(const Sales_data&);
double avg_price() const;
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
//Sales_data的非成员接口函数
Sales_data add(const Sales_data&, const Sales_data&);
ostream& print(ostream&, const Sales_data&);
istream& read(istream&, Sales_data&);
引入this
struct Sales_data {
string isbn() const {
return this->bookNo;//引入this
}
};
引入const成员函数
这段代码string isbn() const在参数列表添加const是表明该成员函数不会修改成员变量
类作用域和成员函数
类本身是一个作用域,类的成员函数定义嵌套在类的作用域之内,即是bookNo定义在isbn之后,isbn也能使用bookNo。因此为编译器先编译成员的声明然后再编译成员函数体,因此函数体可以无须在意成员出现的次序。
在类的外部定义成员函数
成员函数的定义必须与它的声明匹配
double Sales_data::avg_price()const {
double ap = 0;
if (units_sold)
ap = revenue / units_sold;
else {
ap = 0;
}
return ap;
}
定义一个返回this对象的函数
Sales_data& Sales_data::combine(const Sales_data& rhs) {
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;//this是隐式的指向执行该成员函数的对象的指针
}
1.3定义类的非成员函数
将属于类的接口的组成部分但是不属于类本身的非成员函数,放在与类声明的同一个头文件内
定义read和print函数
//输入交易信息
istream& read(istream& is, Sales_data& item) {
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units_sold;
return is;
}
//输出交易信息
ostream& print(ostream& os, const Sales_data& item) {
os << item.isbn() << " " << item.units_sold << " " << item.revenue << " " << item.avg_price();
return os;
}
定义add函数
//接受两个对象作为参数,返回一个新的对象表示两个对象的和
Sales_data add(const Sales_data& item1, const Sales_data& item2) {
Sales_data sum = item1;
sum.combine(item2);
return sum;
}
1.4构造函数
若未对类显示地定义构造函数,那么编译器会隐式地定义一个默认构造函数,称为合成的默认构造函数。若类由其他构造函数则不存在默认构造函数需要我们自行定义。如果类包含有内置类型或者复合类型的成员若未赋予初始值时,可能会执行错误的操作。
定义Sales_data类的构造函数
定义4个不同的构造函数:
(1)一个istream&,从中读取一条交易信息
(2)一个const string&,表示ISBN编号;编译器赋予其他成员默认值
(3)const string&、unsigned、double
(4)空参数列表(如果定义了其他构造函数也必须定义一个默认构造函数)
struct Sales_data {
Sales_data() = default;
Sales_data(const string &s):bookNo(s){}
Sales_data(const string& s, unsigned n, double p) :
bookNo(s), units_sold(n), revenue(p*n){}
Sales_data(istream&);
//新成员:关于Sales_data对象的操作
//bookNo表示ISBN编号
//units_sold表示某本书的销量
//revenue表示这本书的总销售收入
string isbn() const {
return bookNo;
}
Sales_data& combine(const Sales_data&);
double avg_price() const;
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
在类的外部定义构造函数
Sales_data::Sales_data(istream& is) {
read(is, *this);
}
1.5拷贝、赋值和析构
一般类对象间的拷贝、赋值和销毁操作会由编译器生成,但对于一些类编译器生成的版本无法正常工作,特别是当类需要分配类对象之外的资源时。值得注意的是,很多需要动态内存的类能使用vector对象或者string对象管理必要的存储空间。使用vector或string对象能避免分配和释放内存带来的复杂性。
2.访问控制与封装
使用访问说明符加强类的封装性:
定义在public说明符后的成员在整个程序内可被访问,public成员定义类的接口。
定义在private说明符后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了类的实现细节。
class Sales_data
{
public:
Sales_data() = default;
Sales_data(const std::string& s) :bookNo(s) {}
Sales_data(const std::string& s, unsigned n, double p) :bookNo(s), units_sold(n), revenue(p* n) {}
Sales_data(std::istream&);
std::string isbn() const {
return bookNo;
}
Sales_data& combine(const Sales_data&);
private:
double avg_price() const { return units_sold ? revenue / units_sold : 0; }
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
使用了class关键字而非struct开始类的定义,区别是class和struct的默认访问权限不太一样。
类可以在其第一个访问说明符之前定义成员,使用struct关键字则定义在第一个访问说明符之前的成员是public的,相反使用class关键字则这些成员是private的
2.1友元
想要其他类或者函数访问private内的数据成员,方法是令其他成员或者函数称为它的友元
class Sales_data
{
//为Sales_data的非成员函数所做的友元声明
friend Sales_data add(const Sales_data&, const Sales_data&);
friend std::ostream& print(std::ostream&, const Sales_data&);
friend std::istream& read(std::istream&, Sales_data&);
public:
Sales_data() = default;
Sales_data(const std::string& s) :bookNo(s) {}
Sales_data(const std::string& s, unsigned n, double p) :bookNo(s), units_sold(n), revenue(p* n) {}
Sales_data(std::istream&);
std::string isbn() const {
return bookNo;
}
Sales_data& combine(const Sales_data&);
private:
double avg_price() const { return units_sold ? revenue / units_sold : 0; }
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
//Sales_data的非成员接口函数
Sales_data add(const Sales_data&, const Sales_data&);
std::ostream& print(std::ostream&, const Sales_data&);
std::istream& read(std::istream&, Sales_data&);
注意:友元声明只能出现在类定义的内部,但是在类内出现的具体位置不限。同时友元的声明仅仅指定了访问的权限,而非通常意义上的函数声明。如果我们希望类的用户能够调用某个友元函数,还必须额外对函数进行一次声明
3.类的其他特性
3.1定义一个类型成员
除了定义数据和函数成员之外,类还可以自定义某种类型在类中的别名,别名和其他成员一样存在访问限制,可以是public或private的一种:
#include <string>
class Screen
{
public:
typedef std::string::size_type pos;//等同于using pos=std::string::size_type
private:
pos cursor = 0;
pos height = 0, width = 0;
std::string contents;
};
用来定义类型的成员必须先定义后使用,因此类型成员通常出现在类开始的地方。
3.2Screen类的成员函数
#include <string>
class Screen
{
public:
typedef std::string::size_type pos;//等同于using pos=std::string::size_type
Screen() = default;
Screen(pos ht, pos wd, char c) :height(ht), width(wd), contents(ht*wd,c) {};
char get()const { //读取光标处的字符,隐式内联
return contents[cursor];
}
inline char get(pos ht, pos wd) const;//显示内联
Screen& move(pos r, pos c);//能在之后设为内联
private:
pos cursor = 0;
pos height = 0, width = 0;
std::string contents;
};
3.3令成员作为内联函数
定义在类内部的成员函数是自动内联(隐式内联)的,因此Screen的构造函数和返回光标所指字符的get函数默认是inline函数。能在类内部显示声明内联函数同样也能在类的外部使用inline关键字修饰函数的定义。
//将光标移动至指定位置
inline Screen& Screen::move(pos r, pos c) {
pos row = r * width;
cursor = row + c;
return *this;
}
//返回给定行列的字符
char Screen::get(pos r, pos c) const{
pos row = r * width;
return contents[row + c];
}
3.4重载成员函数
和非成员函数一样,成员函数也可以被重载
//两个版本的get()
char get()const { //读取光标处的字符,隐式内联
return contents[cursor];
}
inline char get(pos ht, pos wd) const;//显示内联
3.5可变数据成员
有时我们希望能修改类的某个数据成员,即使是在一个const成员函数内,可以在变量的声明中假如mutable关键字,这个变量称为可变数据成员,其永远不会是const。
class Screen
{
public:
void some_member() const;
private:
mutable size_t access_ctr;//即使在一个const对象内也能修改
};
void Screen::some_member()const {
++access_ctr;//用于记录成员函数被调用的次数
}
3.6类数据成员的初始值
类内初始值必须以符号=或者花括号表示
class Window_mgr {
private:
//这个Window_mgr追踪的screen
//默认情况下,一个Window_mgr包含一个标准尺寸的空白Screen
std::vector<Screen> screens{ Screen(24,80,' ') };
};
3.7返回*this的成员函数
返回的*this是左值也就是返回的是对象本身而不是对象的副本。
class Screen
{
public:
typedef std::string::size_type pos;//等同于using pos=std::string::size_type
Screen() = default;
Screen& set(char);
Screen& set(pos, pos, char);
};
inline Screen& Screen::set(char c) {
contents[cursor] = c;//设置当前光标所在位置的新值
return *this;
}
inline Screen& Screen::set(pos r,pos col,char c) {
contents[r*width+col] = c;//设置给定位置的新值
return *this;
}
若返回*this则能够myScreen.move(4,0).set('#');若定义一个display函数其返回值为const Screen&类型,则myScreen.display(cout).set('#');发生错误。
若想解决这样的问题则可以对其进行重载
class Screen
{
public:
typedef std::string::size_type pos;//等同于using pos=std::string::size_type
Screen() = default;
//根据对象是否是const重载了display函数
Screen& display(std::ostream& os) {
do_display(os);
return *this;
}
const Screen& display(std::ostream& os) const{
do_display(os);
return *this;
}
private:
//负责显示内容
void do_display(std::ostream& os) const { os << contents; }
};
Screen myScreen(5,3);
const Screen blank(5,3)
myScreen.set('#').display(cout);//调用非常量版本
blank.display(cout);//调用常量版本
3.8类类型
每个类定义了唯一的类型。对于两个类来说即是成员完全一样,这两个类也是两个不同的类型。
Sales_data item1;
class Sales_data item1;
3.8类的声明
class Screen ;//类的声明
仅声明而未定义的情况下,可以定义只想这种类型的指针或引用,也可以声明(不可以定义)以不完全类型作为参数或者返回类型的函数。创建类的对象前该类必须被定义过。定义类是允许包含它自身类型的引用或指针:
class Link_screen{
Screen window;
Link_screen *next;
Link_screen *pre;
}
3.9友元再探
类还可以把其他类或者其他类的成员函数定义成友元,友元函数是隐式内联的。
类之间的友元
注意:友元不具有传递性
class Screen
{
public:
typedef std::string::size_type pos;//等同于using pos=std::string::size_type
Screen() = default;
//Window_mgr的成员可以访问Screen类的私有部分
friend class Window_mgr;
private:
pos cursor = 0;
pos height = 0, width = 0;
std::string contents;
};
class Window_mgr {
public:
//窗口中每个屏幕的编号
using ScreenIndex = std::vector<Screen>::size_type;
//按照编号将指定屏幕重置为空白
void clear(ScreenIndex);
private:
//这个Window_mgr追踪的screen
//默认情况下,一个Window_mgr包含一个标准尺寸的空白Screen
std::vector<Screen> screens{ Screen(24,80,' ') };
};
void Window_mgr::clear(ScreenIndex index) {
Screen& s = screens[index];
s.contents = std::string(s.height * s.width, ' ');
}
令成员函数作为友元
还可以只为clear提供访问权限,还必须指出该成员函数属于哪个类:
class Screen
{
public:
typedef std::string::size_type pos;//等同于using pos=std::string::size_type
Screen() = default;
//Window_mgr的成员可以访问Screen类的私有部分
friend void Window_mgr::clear(ScreenIndex);
private:
pos cursor = 0;
pos height = 0, width = 0;
std::string contents;
};
想令某个成员函数作为友元必须按照如下方式设计程序:
(1)首先定义Window_mgr类,声明clear函数,但是不能定义它。在clear使用Screen的成员之前必须先声明Screen
(2)接下来定义Screen,包括对于clear的友元声明
(3)最后定义clear,此时它才可以使用Screen的成员
函数重载和友元
如果有重载函数,要将其作为友元则要每个分别声明
友元声明和作用域
就算在类中定义该友元函数,我们也必须在类的外部提供相应的声明从而使得函数可见,也就是说友元必须是要声明过的。友元声明的作用是影响访问权限。
struct X {
friend void f() {}
X() { f();}//错误:友元函数还未声明
void g();
void h();
};
void X::g() {
return f();
}//错误 f还未声明
void f();
void X::h() {
return f();//正确,现在f的声明在作用域中了
4.类的作用域
每个类都会定义它自己的作用域。在类的作用域之外,普通的数据和函数成员只能由对象、引用或者指针使用成员访问运算符来访问。
作用域和定义在类外部的成员:
一个类就是一个作用域能够很好地解释为什么我们在类的外部定义成员函数时必须同时提供类名和函数名,因为在类的外部成员名被隐藏起来了。
void Window_mgr::clear(ScreenIndex index) {
Screen& s = screens[index];
s.contents = std::string(s.height * s.width, ' ');
}
由于函数的返回类型通常出现在函数名之前,因此当成员函数定义在类的外部时,返回类型中的名字都位于类的作用域之外。这是,必须指明返回类型是哪个类的成员。
class Window_mgr {
public:
//窗口中每个屏幕的编号
using ScreenIndex = std::vector<Screen>::size_type;
//按照编号将指定屏幕重置为空白
ScreenIndex addScreen(const Screen&);
};
//先处理返回类型之后才进入Window_mgr作用域
Window_mgr::ScreenIndex Window_mgr::clear(const Screen &s) {
screens.push_back(s);
return screens.size()-1;
}
4.1名字查找与类的作用域
对于定义在类内部的成员函数来说,解析其名字的方式与普通查找规则有所区别,类的定义分两步处理:
(1)先编译成员的声明
(2)直到类全部可见后才编译函数体
用于类成员声明的名字查找:
类内成员声明中使用的名字,包括返回类型或者参数列表中使用的名字,都必须在使用前确保可见。
typedef double Money;
string bal;
class Account{
public:
Money balance(){return bal;}
private:
Money bal;
}
上述Money的声明不在类中,编译器会及则会接着到Account的外层作用域中查找,另外一方面balance函数中返回的是名为bal的成员而非外层作用域的string对象。
类型名要特殊处理:
外层已经声明并在类中使用的名字,不能在内层再定义该名字。
typedef double Money;
string bal;
class Account{
public:
Money balance(){return bal;}//使用外层作用域的Money
private:
typedef double Money;//错误,不能重复定义Money
Money bal;
}
成员定义中的普通块作用域的名字查找:
成员函数中使用的名字按照如下方式解析:
(1)首先,在成员函数内查找该名字的声明。和之前一样,只有在函数使用之前出现的声明才被考虑。
(2)如果在成员函数内没有找到则在类内继续查找,这时类的所有成员都可以被考虑。
(3)如果类内也没找到该名字的声明,在成员函数定义之前的作用域内继续查找。
一般来说,不建议使用其他成员的名字作为某个成员函数的参数。
下列中先在函数作用域中查找,查找到height是参数名
int height;
class Screen{
public:
typedef std::string::size_type pos;
void dummu_fcn(pos height){
cursor=width*height;//哪个height?是那个参数
}
private:
pos cursor=0;
pos height=0,width=0;
};
如果想要绕开上面的查找规则访问内类成员,可以显示访问
int height;
class Screen{
public:
typedef std::string::size_type pos;
void dummu_fcn(pos height){
cursor=width*this->height;//成员height
//另一种方式
cursor=width*Screen::height;//成员height
}
private:
pos cursor=0;
pos height=0,width=0;
};
如果想访问外围作用域的可以
int height;
class Screen{
public:
typedef std::string::size_type pos;
void dummu_fcn(pos height){
cursor=width*::height;//全局height
}
private:
pos cursor=0;
pos height=0,width=0;
};
5.构造函数再探
5.1构造函数初始值列表
这段代码和原始版本构造函数相同,这和原始版本有什么影响完全依赖于数据成员的类型
Sales_data::Sales_data(const string &s,unsigned cnt,double price){
bookNo=s;
units_sold=cnt;
revenue=cnt*price;
}
构造函数的初始值有时必不可少
有时我们可以忽略数据成员初始化和赋值之间的差异,但如果成员是const或者是引用的话必须初始化。
class ConstRef{
public:
ConstRef(int ii);
//ci和ri必须被初始化
ConstRef(int ii){
//赋值初始化
i=ii; //正确
ci=ii;//错误:不能给const赋值
ri=i;//错误。ri没被初始化
}
//正确
ConstRef::ConstRef(int ii):i(ii),ci(ii),ri(i){}
private:
int i;
const int ci;
int &ri;
}
成员初始化的顺序
一般成员的初始化顺序与它们在类定义中的出现顺序一致,如果一个成员是用另一个成员来初始化的,那么两个成员的初始化顺序就很关键了。
默认实参和构造函数
Sales_data 的默认构造函数的行为与只接受一个string实参的构造函数差不多,因此可以对其重写成一个使用默认实参的构造函数
struct Sales_data {
Sales_data(string s=""):bookNo(s){}
Sales_data(string s, unsigned n, double p) :
bookNo(s), units_sold(n), revenue(p*n){}
Sales_data(istream&);
//新成员:关于Sales_data对象的操作
//bookNo表示ISBN编号
//units_sold表示某本书的销量
//revenue表示这本书的总销售收入
string isbn() const {
return bookNo;
}
Sales_data& combine(const Sales_data&);
double avg_price() const;
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
5.2委托构造函数
委托构造函数指使用它所属类的其他构造函数执行它自己的初始化过程。
struct Sales_data {
//非委托构造函数使用对应的实参初始化成员
Sales_data(string s, unsigned n, double p) :
bookNo(s), units_sold(n), revenue(p*n){}
//其余委托构造函数全都委托给另一个构造函数
Sales_data():Sales_data("",0,0){}
Sales_data(string s):Sales_data(s,0,0){}
Sales_data(istream& is):Sales_data(){read(is,*this);};
};
5.3默认构造函数的作用
当对象被默认初始化或值初始化时自动执行默认构造函数。
默认初始化在以下情况下发生:
(1)当我们在块作用域内不使用任何初始值定义一个非静态变量或数组时
(2)当一个类本身含有类类型的成员且使用合成的默认构造函数时
(3)当类类型的成员没有在构造函数初始值列表中显示地初始化时
值初始化在以下情况下发生:
(1)在数组初始化过程中如果我们提供的初始值数量少于数组的大小时
(2)当我们不使用初始值定义一个局部静态变量时
(3)当我们通过书写形如T()的表达式显示地请求值初始化时,其中T是类型名。
5.4隐式的类类型转换
如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们把这种构造函数称为转换构造函数
string null_book="9-999-99999-9";//相当于省略了 Sales_data item2=Sales_data(null_book);
//构建一个临时的Sales_data对象
//该对象的其他为默认初始值,bookNo=null_book
item.combine(null_book);
只允许一步类类型转换
编译器只会自动地执行一步类型转换例如:
//错误进行了两步类型转换
//(1)把"9-999-99999-9"转为string 类型
//(2)把string 类型转为Sales_data 类型
//该对象的其他为默认初始值,bookNo=null_book
item.combine("9-999-99999-9");
//正确,显示转换成string,隐式转换成Sales_data
item.combine(string("9-999-99999-9"));
//正确,显示转换成Sales_data ,隐式转换成string
item.combine(Sales_data("9-999-99999-9"));
注意:类类型转换不是总有效
抑制构造函数定义的隐式转换
可以通过讲构造函数声明为explicit阻止其隐式转换,explicit只允许出现在类内的构造函数声明处。
explicit构造函数只能用于直接初始化,不可用于拷贝形式的初始化过程。
为转换显示地使用构造函数
尽管编译器不会将explicit的构造函数用于隐式转换过程,但是可以使用构造函数显示地强制进行转换:
string null_book="9-999-99999-9";
//正确,实参是一个显示构造的Sales_data对象
item.combine(Sales_data(null_book));
//正确,static_cast可以使用explicit的构造函数
item.combine(static_cast<Sales_data>(cin));
5.5聚合类
使用户可以直接访问其成员,并且具有特殊的初始化语法形式,当一个类满足以下条件,我们说这个类是聚合类:
(1)所有成员都是public的
(2)没有定义任何构造函数
(3)没有类内初始值
(4)没有基态,也没有virtual函数
struct Data{
int ival;
string s;
};
Data vall={0,"Anna"}//能够使用花括号对其进行初始化,顺序必须一致
5.6字面量常量类
除了算术类型、引用和指针外,某些类也是字面量类型,类内可能含有constexpr函数成员,这样的成员是隐式const的。
数据成员都是字面量类型的聚合类是字面量常量类。如果一个类不是聚合类,但是符合下列要求,则它也是一个字面量常量类:
(1)数据成员都必须是字面量类型。
(2)类必须至少含有一个constexpr构造函数
(3)如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数
(4)类必须使用析构函数的默认定义,该成员负责销毁类的对象
constexpr构造函数
constexpr构造函数可以声明成=default的形式,否则constexpr构造函数就必须初始化所有数据成员.
class Debug{
public:
constexpr Debug(bool b=true):hw(b),io(b),other(b){}
constexpr Debug(bool h,bool i,bool o):hw(h),io(i),other(o){}
constexpr bool any(){ return hw||io||other;}
void set_io(bool b){io=b;}
void set_hw(bool b){hw=b;}
void set_other(bool b){other=b;}
private:
bool hw;
bool io;
bool other;
};
constexpr Debug io_sub(false,true,false);
io_sub.any()
constexpr Debug prod(false);
prod.any()
6.类的静态成员
有的时候类需要它的一些成员与类本身直接相关,而不是与类的各个对象保持关联,如何银行账户类的基准利率。其一旦浮动,我们希望所有的对象都能使用新值。
声明静态成员
在成员的声明之前加上static关键字即可定义静态成员,可以是public或private的。可以是常量、引用、指针、类类型等。
class Account{
public:
void calculate(){amount+=amount*interestRate;}//成员函数不用通过作用域运算符就可直接使用静态成员
static double rate(){return interestRate;}
static void rate(double);
private:
std::string owner;
double amount;
static double interestRate;//静态数据成员
static double initRate();//静态成员函数,不能声明成const,也不能使用this
};
使用静态成员
double r;
r=Account::rate();//使用作用域运算符访问静态成员
Account ac1;
Account *ac2=&ac1;
r=ac1.rate();//通过类的对象、指针或者引用访问静态成员
r=ac2->rate();
定义静态成员
和其他成员函数一样,既可以在类内部也可以在类的外部定义静态成员函数,不用重复static
定义静态成员函数
void Account::rate(double newRate){
interestRate=newRate;
}
double Account::initRate() {
// 实现 initRate() 函数的代码
return 0.05;
}
定义静态数据成员
一般不能在类内定义和初始化静态成员,且一个静态数据成员只能定义一次。
double Account::interestRate=initRate();
静态成员的类内初始化
要想在类内初始化静态成员,要求静态成员必须是字面量常量类型的constexpr,初始值必须是常量表达式,如果在类内提供了一个初始值,则成员的定义不能再指定一个初始值了
class Account{
static constexpr int period=30;
}
constexpr int Account::period;//定义
静态数据成员能作为默认实参
该博客围绕C++展开,涵盖第三章字符串、向量和数组的练习,第四章表达式中运算符、类型转换等知识,第五章语句的各类形式,第六章函数基础、参数传递等内容,以及第七章类的定义、访问控制、构造函数等特性,是C++学习的详细记录。
&spm=1001.2101.3001.5002&articleId=136133501&d=1&t=3&u=bb126caa32c5407599f9ad93f15c2d43)
2807

被折叠的 条评论
为什么被折叠?



