s2=s1;
执行的是浅拷贝。执行完s2=s1;
后,s2.str 和s1.str 指向同一个地方, 如图 1 (b) 所示。这导致 s2.str 原来指向的那片动态分配的存储空间再也不会被释放,变成内存垃圾。
此外,s1 和 s2 消亡时都会执行**delete[] str;
**,这就使得同一片存储空间被释放两次,会导致严重的内存错误,可能引发程序意外中止。
而且,如果执行完s1=s2;
后 又执行s1 = "some";
,则会导致 s2.str 也被释放。
为解决上述问题,需要对做=
再次重载。重载后的的逻辑,应该是使得执行s2=s1;
后,s2.str 和 s1.str 依然指向不同的地方,但是这两处地方所存储的字符串是一样的。再次重载=
的写法如下:
String & String::operator = (const String & s)
{
if(str == s.str)
return * this;
if(str)
delete[] str;
if(s.str){ //s. str不为NULL才执行复制操作
str = new char[ strlen(s.str) + 1 ];
strcpy(str, s.str);
}
else
str = NULL;
return * this;
}
经过重载,赋值号=
的功能不再是浅拷贝,而是将一个对象中指针成员变量指向的内容复制到另一个对象中指针成员变量指向的地方。这样的拷贝就叫“深拷贝”。
程序第 3 行要判断 str==s.str,是因为要应付如下的语句:
s1 = s1;
这条语句本该不改变s1的值才对。**s1=s1;
**等价于**s.operator=(s1)**;
,如果没有第 3 行和第 4 行,就会导致函数执行中的 str 和 s.str 完全是同一个指针(因为形参 s 引用了实参 s1,因此可以说 s 就是 s1)。第 8 行为 str 新分配一片存储空间,第 9 行从自己复制到自己,那么 str 指向的内容就不知道变成什么了。
当然,程序员可能不会写s1=s1;
这样莫名奇妙的语句,但是可能会写rs1=rs2;
,如果 rs1 和 rs2 都是 String 类的引用,而且它们正好引用了同一个 String 对象,那么就等于发生了s1=s1;
这样的情况。
思考题:上面的两个 operator= 函数有什么可以改进以提高执行效率的地方?
重载了两次=
的 String 类依然可能导致问题。因为没有编写复制构造函数,所以一旦出现使用复制构造函数初始化的 String 对象(例如,String 对象作为函数形参,或 String 对象作为函数返回值),就可能导致问题。最简单的可能出现问题的情况如下:
String s2;
s2 = “Transformers”;
String s1(s2);
s1 是以 s2 作为实参,调用默认复制构造函数来初始化的。默认复制构造函数使得 s1.str 和 s2.str 指向同一个地方,即执行的是浅拷贝,这就导致了前面提到的没有对=
进行第二次重载时产生的问题。因此还应该为 String 类编写如下复制构造函数,以完成深拷贝:
String::String(String & s)
{
if(s.str){
str = new char[ strlen(s.str) + 1 ];
strcpy(str, s.str);
}
else
str = NULL;
}
最后,给出 String 类的完整代码:
class String {
private:
char * str;
public:
String() :str(NULL) { }
String(String & s);
const char * c_str() const { return str; };
String & operator = (const char * s);
String & operator = (const String & s);
~String();
};
String::String(String & s)
{
if (s.str) {
str = new char[strlen(s.str) + 1];
strcpy(str, s.str);
}
else
str = NULL;
}
String & String::operator = (const String & s)
{
if (str == s.str)
return *this;
if (str)
delete[] str;
if (s.str) { //s. str不为NULL才执行复制操作
str = new char[strlen(s.str) + 1];
strcpy(str, s.str);
}
else
str = NULL;
return *this;
}
String & String::operator = (const char * s)
//重载"="以使得 obj = "hello"能够成立
{
if (str)
delete[] str;
if (s) { //s不为NULL才会执行拷贝
str = new char[strlen(s) + 1];
strcpy(str, s);
}
else
str = NULL;
return *this;
}
String::~String()
{
if (str)
delete[] str;
};
4 C++运算符重载为友元函数
一般情况下,将运算符重载为类的成员函数是较好的选择。但有时,重载为成员函数不能满足使用要求,重载为全局函数又不能访问类的私有成员,因此需要将运算符重载为友元。
例如,对于复数类 Complex 的对象,希望它能够和整型以及实数型数据做四则运算,假设 c 是 Complex 对象,希望c+5
和5+c
这两个表达式都能解释得通。
将+重载为 Complex 类的成员函数能解释c+5
,但是无法解释5+c
。要让5+c
有意义,则应对+进行再次重载,将其重载为一个全局函数。为了使该全局函数能访问 Complex 对象的私有成员,就应该将其声明为 Complex 类的友元。具体写法如下:
class Complex
{
double real, imag;
public:
Complex(double r, double i):real®, imag(i){};
Complex operator + (double r);
friend Complex operator + (double r, const Complex & c);
};
Complex Complex::operator + (double r)
{ //能解释c+5
return Complex(real+r, imag);
}
Complex operator + (double r, const Complex & c)
{ //能解释5+c
return Complex (c.real+r, c.imag);
}
5 C++实现可变长度的动态数组
实践中经常碰到程序需要定义一个数组,但不知道定义多大合适的问题。按照最大的可能性定义,会造成空间浪费;定义小了则无法满足需要。
如果用动态内存分配的方式解决,需要多少空间就动态分配多少,固然可以解决这个问题,但是要确保动态分配的内存在每一条执行路径上都能够被释放,也是一件头疼的事情。
因此需要编写一个长度可变的数组类,该类的对象就能存放一个可变长数组。该数组类应该有以下特点:
- 数组的元素个数可以在初始化该对象时指定。
- 可以动态往数组中添加元素。
- 使用该类时不用担心动态内存分配和释放问题。
- 能够像使用数组那样使用动态数组类对象,如可以通过下标访问其元素。
程序代码如下:
#include
#include
using namespace std;
class CArray
{
int size; //数组元素的个数
int* ptr; //指向动态分配的数组
public:
CArray(int s = 0); //s代表数组元素的个数
CArray(CArray & a);
~CArray();
void push_back(int v); //用于在数组尾部添加一个元素 v
CArray & operator = (const CArray & a); //用于数组对象间的赋值
int length() const { return size; } //返回数组元素个数
int & operator[](int i)
{ //用以支持根据下标访问数组元素,如“a[i]=4;”和“n=a[i];”这样的语句
return ptr[i];
};
};
CArray::CArray(int s) : size(s)
{
if (s == 0)
ptr = NULL;
else
ptr = new int[s];
}
CArray::CArray(CArray & a)
{
if (!a.ptr) {
ptr = NULL;
size = 0;
return;
}
ptr = new int[a.size];
memcpy(ptr, a.ptr, sizeof(int) * a.size);
size = a.size;
}
CArray::~CArray()
{
if (ptr) delete[] ptr;
}
CArray & CArray::operator=(const CArray & a)
{ //赋值号的作用是使 = 左边对象中存放的数组的大小和内容都与右边的对象一样
if (ptr == a.ptr) //防止 a=a 这样的赋值导致出错
return *this;
if (a.ptr == NULL) { //如果a里面的数组是空的
if (ptr)
delete[] ptr;
ptr = NULL;
size = 0;
return *this;
}
if (size < a.size) { //如果原有空间够大,就不用分配新的空间
if (ptr)
delete[] ptr;
ptr = new int[a.size];
}
memcpy(ptr, a.ptr, sizeof(int)*a.size);
size = a.size;
return this;
}
void CArray::push_back(int v)
{ //在数组尾部添加一个元素
if (ptr) {
int tmpPtr = new int[size + 1]; //重新分配空间
memcpy(tmpPtr, ptr, sizeof(int) * size); //复制原数组内容
delete[] ptr;
ptr = tmpPtr;
}
else //数组本来是空的
ptr = new int[1];
ptr[size++] = v; //加入新的数组元素
}
int main()
{
CArray a; //开始的数组是空的
for (int i = 0; i<5; ++i)
a.push_back(i);
CArray a2, a3;
a2 = a;
for (int i = 0; i<a.length(); ++i)
cout << a2[i] << " ";
a2 = a3; //a2 是空的
for (int i = 0; i<a2.length(); ++i) //a2.length()返回 0
cout << a2[i] << " ";
cout << endl;
a[3] = 100;
CArray a4(a);
for (int i = 0; i<a4.length(); ++i)
cout << a4[i] << " ";
return 0;
}
程序的输出结果为:
0 1 2 3 4
0 1 2 100 4
[]
是双目运算符,有两个操作数,一个在里面,一个在外面。表达式 a[i] 等价于 a.operator。按照[]
原有的特性,a[i]
应该能够作为左值使用,因此 operator[] 函数应该返回引用。
思考题:每次在数组尾部添加一个元素都要重新分配内存并且复制原有内容,显然效率是低下的。有什么办法能够加快添加元素的速度呢?
6 C++重载<<和>>(C++重载输出运算符和输入运算符)
在 C++ 中,左移运算符<<
可以和 cout 一起用于输出,因此也常被称为“流插入运算符”或者“输出运算符”。实际上,<<
本来没有这样的功能,之所以能和 cout 一起使用,是因为被重载了。
cout 是 ostream 类的对象。ostream 类和 cout 都是在头文件 中声明的。ostream 类将<<
重载为成员函数,而且重载了多次。为了使cout<<"Star War"
能够成立,ostream 类需要将<<
进行如下重载:
ostream & ostream::operator << (const char* s)
{
//输出s的代码
return * this;
}
为了使cout<<5;
能够成立,ostream 类还需要将<<
进行如下重载:
ostream & ostream::operator << (int n)
{
//输出n的代码
return *this;
}
重载函数的返回值类型为 ostream 的引用,并且函数返回 *this,就使得cout<<"Star War"<<5
能够成立。有了上面的重载,cout<<"Star War"<<5;
就等价于:
( cout.operator<<(“Star War”) ).operator<<(5);
重载函数返回 *this,使得cout<<"Star War"
这个表达式的值依然是 cout(说得更准确一点就是 cout 的引用,等价于 cout),所以能够和<<5
继续进行运算。
cin 是 istream 类的对象,是在头文件 中声明的。istream 类将>>
重载为成员函数,因此 cin 才能和>>
连用以输入数据。一般也将>>
称为“流提取运算符”或者“输入运算符”。
例题:假定 c 是 Complex 复数类的对象,现在希望写cout<<c;
就能以 a+bi 的形式输出 c 的值;写cin>>c;
就能从键盘接受 a+bi 形式的输入,并且使得 c.real = a, c.imag = b。
显然,要对<<
和>>
进行重载,程序如下:
#include
#include
#include
using namespace std;
class Complex
{
double real,imag;
public:
Complex( double r=0, double i=0):real®,imag(i){ };
friend ostream & operator<<( ostream & os,const Complex & c);
friend istream & operator>>( istream & is,Complex & c);
};
ostream & operator<<( ostream & os,const Complex & c)
{
os << c.real << “+” << c.imag << “i”; //以"a+bi"的形式输出
return os;
}
istream & operator>>( istream & is,Complex & c)
{
string s;
is >> s; //将"a+bi"作为字符串读入, “a+bi” 中间不能有空格
int pos = s.find(“+”,0);
string sTmp = s.substr(0,pos); //分离出代表实部的字符串
c.real = atof(sTmp.c_str());//atof库函数能将const char*指针指向的内容转换成 float
sTmp = s.substr(pos+1, s.length()-pos-2); //分离出代表虚部的字符串
c.imag = atof(sTmp.c_str());
return is;
}
int main()
{
Complex c;
int n;
cin >> c >> n;
cout << c << “,” << n;
return 0;
}
程序的运行结果:
13.2+133i 87
13.2+133i,87
因为没有办法修改 ostream 类和 istream 类,所以只能将<<
和>>
重载为全局函数的形式。由于这两个函数需要访问 Complex 类的私有成员,因此在 Complex 类定义中将它们声明为友元。
cout<<c
会被解释成operator<<(cout, c)
,因此编写 operator<< 函数时,它的两个参数就不难确定了。
第 13 行,参数 os 只能是 ostream 的引用,而不能是 ostream 对象,因为 ostream 的复制构造函数是私有的,没有办法生成 ostream 参数对象。operator<< 函数的返回值类型设为 ostream &,并且返回 os,就能够实现<<
的连续使用,如cout<<c<<5
。在本程序中,执行第 34 行的cout<<c
进入 operator<< 后,os 引用的就是 cout,因此第 34 行就能产生输出。
用 cin 读入复数时,对应的输入必须是 a+bi 的格式,而且中间不能有空格,如输入 13.2+33.4i。第 21 行的is>>s;
读入一个字符串。假定输入的格式没有错误,那么被读入 s 的就是 a+bi 格式的字符串。
读入后需要将字符串中的实部 a 和虚部 b 分离出来,分离的办法就是找出被+
隔开的两个子串,然后将两个字符串转换成浮点数。第 24 行调用了标准库函数 atof 来将字符串转换为浮点数。该函数的原型是float atof(const char *)
,它在 头文件中声明。
7 C++重载()(强制类型转换运算符)
在 C++ 中,类型的名字(包括类的名字)本身也是一种运算符,即类型强制转换运算符。
类型强制转换运算符是单目运算符,也可以被重载,但只能重载为成员函数,不能重载为全局函数。经过适当重载后,(类型名)对象
这个对对象进行强制类型转换的表达式就等价于对象.operator 类型名()
,即变成对运算符函数的调用。
下面的程序对 double 类型强制转换运算符进行了重载。
#include
using namespace std;
class Complex
{
double real, imag;
public:
Complex(double r = 0, double i = 0) :real®, imag(i) {};
operator double() { return real; } //重载强制类型转换运算符 double
};
int main()
{
Complex c(1.2, 3.4);
cout << (double)c << endl; //输出 1.2
double n = 2 + c; //等价于 double n = 2 + c. operator double()
cout << n; //输出 3.2
}
程序的输出结果是:
1.2
3.2
第 8 行对 double 运算符进行了重载。重载强制类型转换运算符时,不需要指定返回值类型,因为返回值类型是确定的,就是运算符本身代表的类型,在这里就是 double。
重载后的效果是,第 13 行的(double)c
等价于c.operator double()
。
有了对 double 运算符的重载,在本该出现 double 类型的变量或常量的地方,如果出现了一个 Complex 类型的对象,那么该对象的 operator double 成员函数就会被调用,然后取其返回值使用。
例如第 14 行,编译器认为本行中c
这个位置如果出现的是 double 类型的数据,就能够解释得通,而 Complex 类正好重载了 double 运算符,因而本行就等价于:
double n = 2 + c.operator double();
8 C++重载++和–(自增和自减运算符)
自增运算符++
、自减运算符--
都可以被重载,但是它们有前置、后置之分。
以++
为例,假设 obj 是一个 CDemo 类的对象,++obj
和obj++
本应该是不一样的,前者的返回值应该是 obj 被修改后的值,而后者的返回值应该是 obj 被修改前的值。如果如下重载++
运算符:
CDemo & CDemo::operator ++ ()
{
//…
return * this;
}
那么不论obj++
还是++obj
,都等价于obj.operator++()
无法体现出差别。
为了解决这个问题,C++ 规定,在重载++
或--
时,允许写一个增加了无用 int 类型形参的版本,编译器处理++
或--
前置的表达式时,调用参数个数正常的重载函数;处理后置表达式时,调用多出一个参数的重载函数。来看下面的例子:
#include
using namespace std;
class CDemo {
private:
int n;
public:
CDemo(int i=0):n(i) { }
CDemo & operator++(); //用于前置形式
CDemo operator++( int ); //用于后置形式
operator int ( ) { return n; }
friend CDemo & operator–(CDemo & );
friend CDemo operator–(CDemo & ,int);
};
CDemo & CDemo::operator++()
{//前置 ++
n ++;
return * this;
}
CDemo CDemo::operator++(int k )
{ //后置 ++
CDemo tmp(*this); //记录修改前的对象
n++;
return tmp; //返回修改前的对象
}
CDemo & operator–(CDemo & d)
{//前置–
d.n–;
return d;
}
CDemo operator–(CDemo & d,int)
{//后置–
CDemo tmp(d);
d.n --;
return tmp;
}
int main()
{
CDemo d(5);
cout << (d++ ) << “,”; //等价于 d.operator++(0);
cout << d << “,”;
cout << (++d) << “,”; //等价于 d.operator++();
cout << d << endl;
cout << (d-- ) << “,”; //等价于 operator-(d,0);
cout << d << “,”;
cout << (–d) << “,”; //等价于 operator-(d);
cout << d << endl;
return 0;
}
程序运行结果:
5,6,7,7
7,6,5,5
本程序将++
重载为成员函数,将--
重载为全局函数。其实都重载为成员函数更好,这里将--
重载为全局函数只是为了说明可以这么做而已。
调用后置形式的重载函数时,对于那个没用的 int 类型形参,编译器自动以 0 作为实参。 如第 39 行,d++
等价于d.operator++(0)
。
对比前置++
和后置++
运算符的重载可以发现,后置++
运算符的执行效率比前置的低。因为后置方式的重载函数中要多生成一个局部对象 tmp(第21行),而对象的生成会引发构造函数调用,需要耗费时间。同理,后置--
运算符的执行效率也比前置的低。
前置++
运算符的返回值类型是 CDemo &,而后置++
运算符的返回值类型是 CDemo,这是因为运算符重载最好保持原运算符的用法。C++ 固有的前置++
运算符的返回值本来就是操作数的引用,而后置++
运算符的返回值则是操作数值修改前的复制品。例如:
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数软件测试工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年软件测试全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上软件测试开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注软件测试)
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
学起的朋友,同时减轻大家的负担。**
[外链图片转存中…(img-8fLI0FvW-1712955609251)]
[外链图片转存中…(img-nqlgXORn-1712955609253)]
[外链图片转存中…(img-7IfP93US-1712955609254)]
[外链图片转存中…(img-MKBSC8Ov-1712955609254)]
[外链图片转存中…(img-2qFRMFfe-1712955609255)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上软件测试开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注软件测试)
[外链图片转存中…(img-Gyrc6R98-1712955609256)]
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!