C++Primer 第五版中文版阅读笔记2~6
第二章 基本内置类型
2.1复合类型
2.2const限定符
const表示一个量的值不能被修改,例如进行如下定义const int buff=512;
留意以下的情况
int i =1;
const int cnt = i;
在VSCode中认为cnt不是一个完整的常量,这是因为认为尽管cnt是一个整型常量,但是它的常量特征只在修改其值时发生作用。下面是一个实例
2.2.1常量与引用
可以将引用绑定到const对象上,这样就是一个对常量的引用,具体声明如下,此时就引用ref而言,他认为自己指向了一个常量,所以它们自觉地不去修改其所指对象的值。与之对应的是,对一个常量引用可能并未指向一个常量。
const int i=1021;
const int &ref =i;
int ci =1;
const int &re = i;
re = 2; //错误,它认为自己指向常量
i=2; //正确,i是一个非常量,可以修改
2.2.2常量与指针
对于常量与指针的关系,要区分的是常量指针和指向常量的指针。常量指针类似于常量引用,认为自己指向的是一个常量;指向常量的指针本身就是一个常量,它的指向是不能再改变的。
int nub=0;
const int *p = &nub; //指向常量的指针
int *const pi = &nub; //常量指针,指向不能变化
const int *const pip = &nub; //指向常量的常量指针
从右向左读,读的时候看const距离谁最近;
2.2.3顶层const和底层const
对于指针而言;顶层const表示指针本身是个常量,底层const表示指针所指的对象是一个常量。
- 当执行对象的拷贝操作时,拷入和拷出的对象必须有着相同的底层const资格。或者二者的数据类型可以转换,一般非常量可以转换为常量,反之则不行。
- 在函数中,顶层 const 不影响传入函数的对象,一个拥有顶层 const 的形参无法和另一个没有顶层 const 的形参区分开(因为二者可以进行类型转换),而底层const不同的话无法进行类型转换所以被看作两个类型
其实这里也就是是否可以用一种const来初始化另一种const的判断
有下面类似的代码示例,这说明可以用非常量初始化一个底层const,但是反过来是不行的。
int i =24;
//成功赋值部分
const int *cp = &i;
const int &cc = i;
const int &ref = 24;
//类型不匹配赋值失败
int *p = cp; //cp包含底层const定义,而p灭有
int &c = cc; //cc包含底层const定义,而c没有
int &r = 24; //不能用字面值初始化非常量引用
2.3 constexpr和常量表达式
常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。字面值就是常量表达式,用常量表达式初始化化的const对象也是常量表达式。
//常量表达式
const int max = 50;
const int limit = max+1;
//不是常量表达式
int size1 = 22;
const int se = size1;
但是在一个复杂系统中很难去欸的那个一个初始值到底是不是常量表达式。C++11标准规定可以使用constexpr
来检查变量的值是否是常量表达式。声明为constexpr
的变量一定是一个常量而且必须用常量表达式初始化。
2.4 auto类型说明符
auto一般会忽略掉顶层const;如果希望推断出的auto是一个顶层const就需要明确指出。设置类型为auto的引用时,初始值的顶层const属性仍然保留。
const int ci = 20;
auto r = ci; //r是一个整数,顶层const特性被忽略了
auto p = &ci; //p是一个整型指针
const auto cr = ci; //cr是cnost int 类型,顶层const
auto &g = ci; //整型常量引用
const auto &j = ci; //常量引用
第三章 string、vector和数组
你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。
3.1 string类型
3.1.1 初始化
string类型的初始化分为直接初始化和拷贝初始化;
3.1.2 string上的操作
- 可以留意string与流的操作,如将s写入到流os中(
os<<s
),从流is中读取字符串到s(is>>s
) - string在读取是会自动忽略头部的空白,并在遇到下一个空白时停止,为了弥补这个可以应用到getline读取string,它在遇到换行符时停止。
实际使用中
getline
得到的换行符会被丢弃,所以由此得到的string字符串并不包含换行符
3.1.3string下标访问
string可以类似数组进行下标访问,其下标类型是string::size_type
类型
3.2.vector类型
3.2.1 初始化
vector的初始化方法比较多,可以通过小括号和中括号进行初始化。一般来说小括号是方法,大括号是值。而且还可以通过数组或者向量进行初始化。
//列表初始化
vector<int> iVec1{
1,2,3,4,5};
vector<string> sVec1{
"sh","yo","we"};
//值初始化
vector<int>iVec2(10); //10个0
vector<string>sVec2(10); //10个空串
//易混淆的
vector<int>iVec3{
10,1}; //两个元素10和1
vector<int>iVec4(10,1); //10个1
vector<string>sVec3{
"HI"};
vector<string>sVec3{
10}; //10个空串
vector<string>sVec3{
10,"HI"}; //10个"HI"
3.2.2
3.3.迭代器
迭代器用来模拟类似于下标的访问;常用的有string与vector支持的迭代器
3.3.1如何使用迭代器
首先来说,需要了解的是迭代器的两个重要的成员(begin和end),需要着重记住的是end成员指向的是尾元素的下一个位置。在C++11的标准中对于迭代器的声明一般使用auto,这样可以避免很多的人为错误。迭代器的运算符操作比较简单这里不再赘述。下面举一个简单使用迭代器的例子
//代码的功能是将字符串s的字母全部变为大写
string s = "hello world";
for(auto it =s.begin();s!=s.end();++it){
*it = toupper(*it)
}
关于迭代器的类型问题:
- 虽然在一般使用中我们可以使用auto来进行迭代器的类型说明,但是还是需要了解到一些具体概念,比如对于int数据类型组成的vector而言,它的迭代器类型就是
vectot<int>::iterator
. - 对于迭代器而言也有着类似于常量指针的常量迭代器,如上述对应的就是
vectot<int>::const_iterator
类型.前者可以通过迭代器进行读写,后者则只能进行读。其实,在理解上我们可以确实将迭代器当作一个指针来操作,就像上面的示例代码中的对s的特定字符的操作就需要用*
号来进行解引用。
注意:但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素,对于这一点有以下几种解释:
1.当插入元素后,end操作返回的迭代器是失效的;
2.插入一个元素后,capacity返回值也会改变
3.进行删除操作时,指向删除点的迭代器全部失效
下面是一个利用迭代器将一段话text转为大写的代码例子
#include <iostream>
#include <string>
#include <iomanip>
#include<vector>
using namespace std;
int main(){
vector<string> text;
string s;
while(getline(cin,s)){
text.push_back(s);
for(auto iter = text.begin(); iter!=text.end() && !iter->empty();iter++){
for(auto it = (*iter).begin();it!=iter->end();it++){
*it = toupper(*it);
}
cout<<*iter;
}
}
}
3.3.2迭代器的运算
- 迭代器的运算一般指迭代器的前进后退操作,这一般是通过重载的运算符+和-等实现的,使用比较简单。类似于数值的运算。
- 只要两个迭代器指向的是同一个容器中的元素位置,就能够进行相减得到二者之间的距离,对于这个距离有个专门的类型
difference_type
,注意这是个带符号类型,如以下使用
vector<string>::iterator i1 = src.begin();
vector<string>::iterator i2 = src.end();
vector<string>::difference_type diff = i2 - i1;
下面是一个利用迭代器进行二分查找的C++实例,需要注意的是,利用迭代器直接进行相加操作时不正确的。如我们在常用的二分查找中使用迭代器的话,对于mid的计算是mid=begin+(end-begin)/2;
3.4 数组
数组类似于vector,通过下标进行访问,但是不同的是它不能进行动态元素添加。而且不同于vector的是,数组不可以进行拷贝和复制,也就是不能通过一个数组来初始化另一个数组。如以下代码就是错误的;
另外在数组的学习过程中对于一些比较复杂的数组声明应该加以着重理解。一般来说按照数组的名字开始从内向外开始顺序阅读。
在数组的学习中应该理解一下字符数组的特殊性,如果用一个字符串来初始化字符数组,则其最后一个元素是一个空字符。
char a[] = {
'C','+','+','\0'}
char a1[3] = "HEL"; //错误,没有空间放空字符'\0'
char a2[] = "HEL"; //{'H','E','L','\0'}
3.4.1指针和数组
提到指针与数组是因为在大多数表达式中,数组类型的对象其实就是一个指向数组首元素的指针。另外,对于下面的数组nums
,有一些概念需要我们把握。首先我们要知道如何根据数组名作为一个指针来取数组的值。
下面第三行代码中的*p
是指向了数组首元素的指针,对其进行加一,是以元素int大小为标准进行内存地址的计算。&nums[0]
以及数组名nums
也是类似的。但是对于&nums
,首先我们前面已经提到数组名本身就是一个指针、&
是一个取地址符,对指针nums
再进行取地址是可行的吗?这样理解的话就有些片面了。直接使用&nums的话,此时的nums就被看作了一个普通的变量,这个取值的结果也就是求出了数组的首地址。注意体会数组首元素的地址和数组的首地址的区别。对于数组首地址加一的话,此时的跳跃值就是整个数组所占的内存大小。所以这也就是为什么下面的&nums+1
的输出是0x277e9ff7f0+20=0x277e9ff804
的缘故。
需要牢记的是,使用数组的时候其实真正使用的是指向数组元素的指针。
int nums[] = {
1,2,3,4,5};
int *p=nums;//等价于string *p=&nums[0];
cout<<*p<<" "<<*(p+1)<<endl; //输出one two
cout<<nums<<endl; //输出0x277e9ff7f0
cout<<&nums[0]<<endl; //输出0x277e9ff7f0
cout<<&nums[0] +1 <<endl; //输出0x277e9ff7f4
cout<<nums+1<<endl; //输出0x277e9ff7f4
cout<<&nums+1<<endl; //输出0x277e9ff804
指针其实也是迭代器,提到指针我们就很容易想到迭代器这个类型,其实数组的指针就相当于一个迭代器。迭代器是有首尾元素的,也可以根据这个求出数组的首尾指针,类似的原理就是对于数组的尾指针也是指向最后一个元素的下一个位置(下标不存在的元素)。为了防止我们在自己使用时候出错,C++11引入了类似迭代器首尾元素的两个标准库函数,分别是begin和end。
int ia[]={
1,2,3,4,5,6,7,8,9,10};
int *beg = begin(ia); //ia首元素的指针
int *en = end(ia); //ia尾元素的下一个位置指针
while(beg!=en){
cout<<*beg;
beg++;
}
另外不同于迭代器的是,数组元素的指针二者相减的数据类型是ptrdiff_t
我们需要记住的是,当两个指针指向同一个元素(数组或者向量)时,二者就是可以比较的。比如上面代码的第四行就等价于while(beg<en)
有一个比较无用的概念:对于内置类型来说,下标的值可以不是无符号类型,而标准库类型(如vector和string)的下标就必须是无符号类型。
- 复杂的数组声明比较难懂从数组的名字开始由内到