文章目录
- 一、引用和指针
- 二、String
-
- 1.初始化String对象的方式
- [2. string对象上的操作](#2. string对象上的操作)
- 3.使用getline读取一整行
- 4.字面值和字符串相加
- 5.使用for循环改变字符串中的字符
- 三、Vector
- 四、迭代器
- 五、数组
- 六、函数
- 七、类
- 其他
-
- [Rvalue references 右值引用](#Rvalue references 右值引用)
- 内存管理
- [可变参数模板 Variadic Templates](#可变参数模板 Variadic Templates)
一、引用和指针
1.引用
一般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值绑定(bind)在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。
引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。
定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的:
c
refval = 2; // 把2赋给 refVal 指向的对象,此处即是赋给了ival
int ii = refVal;//与ii=ival执行结果一样
2.指针
指针(pointer)是"指向(point to)"另外一种类型的复合类型。与引用类似,指针也实现了对其他对象的间接访问。然而指针与引用相比又有很多不同点。其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。其二,指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
2.1利用指针访问对象
如果指针指向了一个对象,则允许使用解引用 符(操作符*)来访问该对象:
c
int ival = 42;
int *p = &ival; //p存放着变量ival的地址,或者说p是指向变量ival的指针
cout << *p; // 由符号*得到指针p所指的对象,输出 42
对指针解引用会得出所指的对象,因此如果给解引用的结果赋值,实际上也就是给指针既指的对象赋值:
c
*p = 0; // 由符号*得到指针p所指的对象,即可经由P为变量ival赋值
cout << *p; //输出0如上述程序所示,为*p赋值实际上是为p所指的对象赋值。
<!--解引用操作仅适用于那些确实指向了某个对象的有效指针。-->
2.2指针的值或指针所指对象的值的改变
有时候要想搞清楚一条赋值语句到底是改变了指针的值还是改变了指针所指对象的值不太容易,最好的办法就是记住赋值永远改变的是等号左侧的对象。当写出如下语句时:
c
pi = &ival; // pi 的值被改变,现在pi 指向了ival
意思是为pi 赋一个新的值,也就是改变了那个存放在pi内的地址值。相反的,如果写出如下语句:
c
*pi = 0; // ival的值被改变,指针pi并没有改变
则*pi(也就是指针pi指向的那个对象)发生改变。
3.赋值和指针
指针和引用都能提供对其他对象的间接访问,然而在具体实现细节上二者有很大不同,其中最重要的一点就是引用本身并非一个对象 。一旦定义了引用,就无法令其再绑定到另外的对象 ,之后每次使用这个引用都是访问它最初绑定的那个对象 。
指针和它存放的地址之间就没有这种限制了。和其他任何变量(只要不是引用)一样,
给指针赋值就是令它存放一个新的地址,从而指向一个新的对象:
c
int i =42;
int *pi =0; //pi被初始化,但没有指向任何对象
int *pi2 = &i; //pi2被初始化,存有i的地址
int *pi3; //如果pi3定义于块内,则pi3的值是无法确定的
pi3= pi2; //pi3和pi2指向同一个对象i
pi2 = 0; //现在pi2不指向任何对象了
4.指向指针的引用
引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在
指向指针的引用:
c
int i=42;
int *pi ; // p是一个int型指针
int *&r =pi; //r是一个对指针p的引用
r = &i; //r引用了一个指针,因此给上赋值&i就是令p指向i
*r =0; //解引用r得到i,也就是p指向的对象,将i的值改为0
要理解r的类型到底是什么,最简单的办法是从右向左阅读r的定义。离变量名最近的符号(此例中是&r的符号&)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针。最后,声明的基本数据类型部分指出r引用的是一个int指针。
二、String
1.初始化String对象的方式
c
string s1 //默认初始化,s1是一个字符串
string s2(s1) //s2是s1的副本
string s2 == s1 //等价于上面
string s3("value") //s3是字面值value的副本,除了最后一个空字符外
string s3 = "value" //等价于上面
string s4(n,'c') //n个连续的c组成的串
2. string对象上的操作
c
os<<s //将s写到输出流os当中,返回os
is>>s //从is中读取字符串赋给s,字符串以空白分隔,返回is
getline(is, s)//从is中读取一行赋给s,返回is
s.empty() //s为空返回true,否则返回false
s.size() //返回s中字符的个数
s[n] //返回s中第n个字符的引用,位置n从0计起
s1+s2 //返回s1和s2连接后的结果
sl=s2 //用s2的副本代替s1中原来的字符
s1==s2 //如果s1和s2中所含的字符完全一样,则它们相等;string对象的相
s1!=s2 //等性判断对字母的大小写敏感
<,<=,>,>= //利用字符在字典中的顺序进行比较,且对字母的大小写敏感
第1章曾经介绍过,使用标准库中的 iostream 来读写int、double等内置类型的值。同样,也可以使用IO操作符读写string对象:
c
//注意:要想编译下面的代码还需要适当的#include语句和using声明
int main()
{
string s;
cin >> s; //如果程序的输入是" Hello World! "
cout << s << endl; //则输出将是"Hello",输出结果中没有任何空格。
return 0;
}
c
string s1, s2; // 把第一个输入读到 s1 中,第二个输入读到 s2 中
cin >> s1 >> s2; //输出两个 string对象
cout << s1 << s2 << endl;//输出HelloWorld!
3.使用getline读取一整行
有时我们希望能在最终得到的字符串中保留输入时的空白符,这时应该用 getline函数代替原来的>>运算符。getline函数的参数是一个输入流和一个string对象,函数从给定的输入流中读入内容,直到遇到换行符为止(注意换行符也被读进来了),然后把所读的内容存入到那个string对象中去(注意不存换行符)。getline只要一遇到换行符就结束读取操作并返回结果,哪怕输入的一开始就是换行符也是如此。如果输入真的一开始就是换行符,那么所得的结果是个空string。
和输入运算符一样,getline 也会返回它的流参数。因此既然输入运算符能作为判断的条件(Ctrl+D/Z),我们也能用getline 的结果作为条件。例如,可以通过改写之前的程序让它一次输出一整行,而不再是每行输出一个词了:
c
int main()
{
string line; //每次读入一整行,直至到达文件末尾
while (getline(cin, line))
cout << line << endl;
return 0;
}
因为line中不包含换行符,所以我们手动地加上换行操作符。和往常一样,使用end1结束当前行并刷新显示缓冲区。
4.字面值和字符串相加
c
string s4 = s1 + ","; //正确;把一个string对象和一个字面值相加
string s5 = "hello" + ","; //错误;两个运算的对象都不是string
string s6 = s1 + "hello" + "," ;//正确;每个加法运算符都有一个string对象
string s7 = "hello" + "," + s1; //错误;不能将两个字面值相加
5.使用for循环改变字符串中的字符
c
string s("Hello World!!!");//转换成大写形式。
for (auto &c :s) // 对于s中的每个字符(注意:C是引用)
c = toupper(c); //c是一个引用,因此赋值语句将改变s中字符的值
cout << s << endl;
使用下标执行迭代
另一个例子是把s的第一个词改成大写形式:
c
//依次处理s中的字符直至我们处理完全部字符或者遇到一个空白
for (decltype(s.size())index =0; index != s.size() && !isspace(s[index]); ++index)
s[index] = toupper(s[index]); //将当前字符改成大写形式
//index的类型是由 decltype关键字决定的,保证下标小于size()的值就可以了。
三、Vector
1.定义和初始化vector对象
c
vector<T> vl //v1是一个空vector,它潜在的元素是T类型的,执行默认初始化
vector<T> v2(v1) //v2中包含有v1所有元素的副本
vector<T> v2 = vl //等价于v2(v1),v2中包含有v1所有元素的副本
vector<T> v3(n, val)//v3包含了n个重复的元素,每个元素的值都是val
vector<T> v4(n) //v4包含了n个重复地执行了值初始化的对象
vector<T> v5{a,b,c.} //v5包含了初始值个数的元素,每个元素被赋予相应的初始值
vector<T> v5 = {a,b, c.} //等价于v5{a,b,c...}
四、迭代器
标准容器迭代器的运算符
*iter 返回迭代器iter所指元素的引用
iter->mem 解引用iter并获取该元素的名为mem的成员,等价于(*iter).mem
++iter 令iter指示容器中的下一个元素
--iter 令iter指示容器中的上一个元素
iterl == iter2 判断两个迭代器是否相等(不相等),如果两个迭代器指示的是同一个元
iterl != iter2 素或者它们是同一个容器的尾后迭代器,则相等;反之,不相等
和指针类似,也能通过解引用迭代器来获取它所指示的元素,执行解引用的迭代器必须合法并确实指示着某个元素。试图解引用一个非法迭代器或者尾后迭代器都是未被定义的行为。
读取text文本数据:
c
// 依次输出 text 的每一行直至遇到第一个空白行为止
for (auto it = text.cbegin();
it != text.cend() && !it->empty();++ it)
cout << *it << endl;
迭代器的运算
二分查找法
c
auto mid = vi.begin() + vi.size()/2
五、数组
初始化
我们可以用字符串字面值对此类数组初始化。当使用这种方式时,一定要注意字符串字面值的结尾处还有一个空字符,这个空字符也会像字符串的其他字符一样被拷贝到字符数组中去:
c
char al[l] = {'c','+'} // 列表初始化,没有空字符
char a2[] = {'c','+','\0'} //列表初始化,含有显式的空字符
char a3[] = "C++" //自动添加表示字符串结束的空字符
const char a4[6] = "Daniel"; //错误:没有空间可存放空字符!
尽管字符串字面值"Daniel"看起来只有6个字符,但是数组的大小必须至少是7,其中6个位置存放字面值的内容,另外1个存放结尾处的空字符。
不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值:
c
int a[] = {1,2,3};
int a2[] = a; //err
a2 = a; //err
理解复杂的数组声明
int *ptrs[10];
int (*Parray)[10] = &arr;
int (&arrRef)[10] = arr;
默认情况下,类型修饰符从右向左依次绑定。对于ptrs来说,从右向左(参见2.3.3节,第52页)理解其含义比较简单:首先知道我们定义的是一个大小为10的数组,它的名字是ptrs,然后知道数组中存放的是指向int的指针。
但是对于Parray来说,从右向左理解就不太合理了。因为数组的维度是紧跟着被声明的名字的,所以就数组而言,由内向外阅读要比从右向左好多了。由内向外的顺序可帮助我们更好地理解Parray 的含义:首先是圆括号括起来的部分,*Parray 意味着Parray是个指针,接下来观察右边,可知道Parray 是个指向大小为10的数组的指针,最后观察左边,知道数组中的元素是int。这样最终的含义就明白无误了,Parray是一个指针,它指向一个 int 数组,数组中包含 10个元素。同理,(&arrRef)表示arrRef是一个引用,它引用的对象是一个大小为10的数组,数组中元素的类型是int.
*
当然,对修饰符的数量并没有特殊限制:
c++
int*(&arry)[10]-ptrs;// arry是数组的引用,该数组含有10个指针
按照由内向外的顺序阅读上述语句,首先知道arry是一个引用,然后观察右边知道,arry引用的对象是一个大小为10的数组,最后观察左边知道,数组的元素类型是指向int的、指针。这样,arry就是一个含有10个int型指针的数组的引用。
要想理解数组声明的含义,最好的办法是从数组的名字开始按照由内向外的顺序阅读。
数组和指针
数组的元素也是对象,对数组使用下标运算符得到该数组指定位置的元素。因此像其他对象一样,对数组的元素使用取地址符就能得到指向该元素的指针:
c
string nums[] =("one","two","three"};//数组的元素是string对象
string *p= &nums[0]; //p指向nums的第一个元素
//然而,数组还有一个特性:在很多用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针:
string *p2 = nums;//等价于p2 = &nums[0]
++p2; //p2指向nums[2]
由上可知,在一些情况下数组的操作实际上是指针的操作,这一结论有很多隐含的意思。其中一层意思是当使用数组作为一个auto变量的初始值时,推断得到的类型是指针而非数组(decltype可避免):
c
int ia[]= (0,1,2,3,4,5,6,7,8,9}; // ia是一个含有10个整数的数组
auto ia2(ia); // ia2 是一个整型指针,指向ia的第一个元素
ia2 = 42; // 错误:ia2是一个指针,不能用int 值给指针赋值
指针也是迭代器
六、函数
1.引用传递和值传递
和其他变量一样,形参的类型决定了形参和实参的交互的方式。如果形参是引用类型,他将绑定到对应的形参上;否则,将实参的值拷贝后赋给形参,形参和实参是两个独立的对象。
2.传值参数
指针形参
指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针使我们可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值:
c
int n = 0,i=42;
int *p= &n, *q= &i; //p指向n;q指向主
*p=42; //n的值改变;P不变
P=q; //p现在指向了主;但是主和n的值都不变
指针形参的行为与之类似:
c
//该函数接受一个指针,然后将指针所指的值置为0
void reset(int *ip)
{
*ip=0; //改变指针ip所指对象的值
ip=0; //只改变了ip的局部拷贝,实参未被改变
}
调用reset 函数之后,实参所指的对缘被置为0,但是实参本身并没有改变:
c
int i=42;
reset(&i); //改变主的值而非土的地址
cout << "i" << i << endl; //输出i=0
如何使用指针形参交换两个整数的值
c
#include <iostream>
using namespace std;
int swap(int *a,int *b);
int main()
{
int *p,*q;
int min=10;
int max=20;
p=&min;
q=&max;
cout<<"交换前:a= "<<min<<",b= "<<max<<endl;
swap(p,q);
cout<<"交换后:a= "<<min<<",b= "<<max<<endl;
return 0;
}
int swap(int *a,int *b)
{
int c;
c=*a;
*a=*b;
*b=c;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R6OUs0vx-1691733924503)(C:\Users\lijiale\AppData\Roaming\Typora\typora-user-images\image-20230718110415925.png)]
p和q分别是两个数的地址,通过对地址取内容,从而得到该数的值,交换的时候也应该是将指针指向的数进行交换。但是但是如果写成下面的形式,就无法进行交换。
c
#include <iostream>
using namespace std;
int swap(int *a,int *b);
int main()
{
int *p,*q;
int min=10;
int max=20;
p=&min;
q=&max;
cout<<"交换前:a= "<<min<<",b= "<<max<<endl;
swap(p,q);
cout<<"交换后:a= "<<min<<",b= "<<max<<endl;
return 0;
}
int swap(int *a,int *b)
{
int *c;
c=a;
a=b;
b=c;
}
这是因为在swap函数体内部,只是交换了两个形参指针本身的值,未能影响实参。这时候如果在形参里面也有一条打印语句的话,能够清晰的看到在被调函数里面,确实交换了指针的值,但是这只在局部范围内有效,调用完毕回到主函数就失效了。
3.传引用参数
通过使用引用形参,允许函数改变一个或多个实参的值。
使用引用避免拷贝。当函数无须修改引用形参的值时最好使用常量引用。
c++
bool isShorter(const string &s1,const string &s2)
{
return s1.size() < s2.size();
}
使用形参隐式返回额外信息
一种方法是定义一个新的数据类型,让它包含位置和数量两个成员。还有另一种更简单的方法,我们可以给函数传入一个额外的引用实参,令其保存字符出现的次数:
c++
//返回s中c第一次出现的位置索引
//引用形参occurs负责统计c出现的总次数
string::size _type find char(const string &s, char c,
string::size _type &occurs)
{
auto ret = s.size(); // 第一次出现的位置(如果有的话)
occurs = 0; //设置表示出现次数的形参的值
for (decltype(ret) i = 0;!= S.size();++i) {
if (s[il == c){
if (ret == S.size())
ret = i; //记录c第一次出现的位置
++occurs; //将出现的次数加1
}
}
return ret; //出现次数通过occurs 隐式地返回
}
当我们调用find_char 函数时,必须传入三个实参:作为查找范围的一个 string 对像要找的字符以及一个用于保存字符出现次数的 size_type。
4.数组形参
c++
int a[] = {0,1,2}; //含有3个整数的数组
int a2[]=a ; //错误:不允许使用一个数组初始化另一个数组
a2 = a; //错误:不能把一个数组直接赋值给另一个数组
所以无法以值传递的方式使用数组参数。
但因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
尽管不能以值传递的方式传递数组,但是我们可以把形参写成类似数组的形式:
c++
//尽管形式不同,但这三个print 函数是等价的
//每个函数都有一个const int*类型的形参
void print(const int*);
void print(const int []); //可以看出来,函数的意图是作用于一个数组
void print(const int[10]); // 这里的维度表示我们期望数组含有多少元素,实际不一定
//尽管表现形式不同,但上面的三个函数是等价的:每个函数的唯一形参都是const int*类型的。当编译器处理对 print 函数的调用时,只检查传入的参数是否是const int*类型:
int i=0,j[2] ={0,1);
print(&i); // 正确:&i的类型是int*
print(j); // 正确:j转换成 int*并指向j[0]
如果我们传给print 函数的是一个数组,则实参自动地转换成指向数组首元素的指针,数组的大小对函数的调用没有影响。
5.返回数组指针
因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用(参见五、复杂)。虽然从语法上来说,要想定义一个返回数组的指针或引用的函数比较烦琐,但是有一些方法可以简化这一任务,其中最直接的方法是使用类型别名:
c++
typedef int arrT[10];// arrT是一个类型别名,它表示的类型是含有10个整数的数组
using arrT = int[10];// arrT的等价声明
arrT* func(int i); //func返回一个指向含有10个整数的数组的指针
其中arrT 是含有10个整数的数组的别名。因为我们无法返回数组,所以将返回类型定义成数组的指针。因此,func 函数接受一个int 实参,返回一个指向包含10个整数的数组的指针。
声明一个返回数组指针的函数
要想在声明func时不使用类型别名,我们必须牢记被定义的名字后面数组的维度:
c++
int arr[10];// arr是一个含有10个整数的数组
int *pl[10];//p1是一个含有10个指针的数组
int (*p2)[10] = &arr;//p2是一个指针,它指向含有10个整数的数组
和这些声明一样,如果我们想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后。然而,函数的形参列表也跟在函数名字后面且形参列表应该先于数组的维度。因此,返回数组指针的函数形式如下所示:
c++
Type (*function (parameter _list) ) [dimension]
类似于其他数组的声明,Type 表示元素的类型,dimension 表示数组的大小。(*function(parameter _list))两端的括号必须存在,就像我们定义p2时两端必须有括号一样。如果没有这对括号,函数的返回类型将是指针的数组。
举个具体点的例子,下面这个func函数的声明没有使用类型别名:
int (*func(int i))[10];
可以按照以下的顺序来逐层理解该声明的含义:
func(int i)表示调用func函数时需要一个int类型的实参。
(*func(int i))意味着我们可以对函数调用的结果执行解引用操作。
(*func (int i))[10]表示解引用func的调用将得到一个大小是10的数组。
int (*func (int i))[10] 表示数组中的元素时int类型
改进:尾置返回类型
c++
auto func(int i) -> int(*)[10];
6.函数重载
不允许两个函数除了返回类型外其他所有要素都相同。
七、类
一个const成员函数如果以引用的形式返回*this,,那么它的返回类型将是常量引用。
的声明,Type 表示元素的类型,dimension 表示数组的大小。(*function(parameter _list))两端的括号必须存在,就像我们定义p2时两端必须有括号一样。如果没有这对括号,函数的返回类型将是指针的数组。
举个具体点的例子,下面这个func函数的声明没有使用类型别名:
int (*func(int i))[10];
可以按照以下的顺序来逐层理解该声明的含义:
func(int i)表示调用func函数时需要一个int类型的实参。
(*func(int i))意味着我们可以对函数调用的结果执行解引用操作。
(*func (int i))[10]表示解引用func的调用将得到一个大小是10的数组。
int (*func (int i))[10] 表示数组中的元素时int类型
改进:尾置返回类型
```c++
auto func(int i) -> int(*)[10];
6.函数重载
不允许两个函数除了返回类型外其他所有要素都相同。
一个const成员函数如果以引用的形式返回*this,,那么它的返回类型将是常量引用。
因为非常量版本的函数对于常量对象是不可用的,所以只能在一个常量对象上调用const成员函数。
其他
Rvalue references 右值引用
非必要的拷贝 unnecessary copying = steal 偷
右值:
c++
//int实验
a + b = 42;//err
//string实验
string s1("Hello");
string s2("World");
s1 + s2 = s2;//pass,s1 = Hello,s2 = World
string() = "World";//pass 临时对象也是右值
//complex实验
complex<int> c1(3,8),c2(1,0);
c1 + c2 = complex<int>(4,9); //pass,c1:(3,8),c2:(1,0)
complex<int>() = complex<int>(4,9);//pass
不可对右值进行操作:
foo()返回的是值(int),在对右值进行取地址操作&foo(),是不允许的
用法:
对于insert操作,提供了两个函数:
1.copy
insert(...,&x) //x是普通的值
2.move
insert(...,&&x) //x是右值(临时对象),此时取右值引用
由于vector放的是Mystring,于是class Mystring 有两个构造ctor: move是浅拷贝
深拷贝与浅拷贝:
内存管理
可变参数模板 Variadic Templates
例一、可变模板参数(递归调用)
max函数比大小
处理print函数
例二、递归继承
处理的是类型(type),使用的是class template