C++基础知识(八)---函数返回值(返回值,返回指针,返回对象,返回引用)---引用---复制构造函数(拷贝构造函数)---深复制与浅复制
一、函数返回值
1.返回值:
int test () { int a=1; return a; }
- 返回值时最简单的方式,它的操作主要在栈上,变量a在函数结束后会删除,为了返回a的值,系统会在内部建立一个临时变量保存a的值,以返回给调用该函数的表达式,调用结束后变量便不再存在。如果a是简单地数据类型也无所谓,不是很占用内存,如果a是大的自定义类型的数据,那么对a的复制将会占用比较大的内存。函数返回值是右值,不能进行运算符操作。
2.返回指针:
-
int *test2() { int *b=new int(); *b=2; return b; }
- 返回指针是在C中除了返回值以外的唯一方式,根据函数栈的特性,也会产生复制,只是复制的是一个指针即一个地址,对于返回大型对象可以减少不少的资源消耗。但返回指针的资源的清理交给了调用者,这违反了谁申请谁销毁的原则。指针也是右值同样无法操作。
3.返回引用:
-
int& test2() { int *b=new(); *b=2; return b; }
- 引用是值的别名,和指针一样不存在对大对象本身的复制,只是引用别名的复制。引用是左值,可以直接进行操作,也可以进行连续赋值,最经典的实例是拷贝构造函数与运算符重载一般都返回引用。
- 需要注意的是局部变量不能作为引用返回。
- C++中成员函数返回对象与返回引用
- 返回对象与返回引用的区别:函数原型与函数头
- Car run(const Car&)//返回对象
- Car& run(const Car&)//返回引用
- 返回对象涉及到生成对象的副本。因此返回对象的成本包括了调用复制构造函数来生成副本所需要的时间和调用析构函数删除副本所需要的时间。返回引用可以节省时间和内存。直接返回对象与函数直接return a返回值一样。都会生成临时副本。
- 返回对象与返回引用的区别:函数原型与函数头
二、C++中的引用
1.引用的引入:
参数的传值方式在函数域中为参数重新分配内存,而把实参的数值传递到新分配的内存中,它的优点是可以有效避免函数的副作用。
当要求改变实参的值的时候,如果实参是一个非常复杂的对象,重新分配内存会引起程序的执行效率的下降。便引出了引用。
2.引用的定义:
类型 & 引用变量名=已经定义过的变量名。
主要用于函数之间的数据传递。对于数组只能引用数组元素,不能引用数组本身(数组本身为地址)
3.引用的使用
- 引用作为函数的参数(形参),采用引用调用时,将对实参进行操作
- 引用作为函数的返回值。一般函数返回值时,要生成一个临时变量作为返回值的副本,而采用引用作为返回值时,不生成值的副本。采用引用返回方式时,不再是返回表达式的值,而是变量。同时返回的不能是函数中的局部变量,这时返回的局部变量的地址已经失效。引用方式返回最常用的是由引用参数传递过来的变量,其次是全局变量,这样返回的变量地址才是有效的
- 返回值为引用的函数作为左值
- 代码示例1:引用作为函数的形参
-
1 void swap(double & d1,double & d2) 2 { 3 double temp ; 4 temp=d1 ; 5 d1=d2 ; 6 d2=temp ; 7 } 8 9 int main(void) 10 { 11 double x , y ; 12 cout<<"请输入x和y的值" 13 <<'\n'; 14 cin>>x>>y ; 15 swap(x,y) ; 16 cout<<"x="<<x<<'\t' 17 <<"y="<<y<<'\n'; 18 return 0; 19 }
- 代码示例2引用作为返回值
-
1 double temp; //全局变量 2 double fsqr1(double a) 3 { 4 temp=a*a ; return temp; 5 } 6 7 double & fsqr2(double a) 8 { 9 temp=a*a ; return temp; 10 } 11 12 int main() 13 { 14 double x=fsqr1(5.5); //第一种情况 15 double y=fsqr2(5.5); //第二种情况 16 cout<<"x="<<x<<'\t‘<<"y="<<y<<endl; 17 return 0; 18 }
fsqr1先是将temp值赋值给内存中建立的无名临时变量,回到主函数后,赋值表达式x=fsqr1(5.5)把临时变量的值赋给x,无名临时变量的生命期结束。
fsqr2没有临时变量的过渡,而是直接返回temp本身赋值给y。不产生副本,效率提高了,但返回值不再是表达式。 - 返回值为引用的函数作为左值:
- 统计学生成绩,分数在80分以上的为A类,60分以上80分以下的为B类,60分以下的为C类
-
1 int& level(int grade ,int& typeA ,int& typeB ,int& typeC) 2 { 3 if(grade>=80) return typeA ; 4 else if(grade>=60) return typeB; 5 else return typeC; 6 } 7 8 void main( ) 9 { 10 int typeA=0,typeB=0,typeC=0,student=9 ; 11 int array[9]={90 , 75 , 83 , 66 , 58 , 40 , 80 , 85 , 71} ; 12 for (int i=0 ; i<student ; i++) 13 level(array[i], typeA, typeB, typeC)++ ; //返回值为引用的函数作为左值 14 cout<<"A类学生数:"<<typeA<<endl ; 15 cout<<"B类学生数:"<<typeB<<endl ; 16 cout<<"C类学生数:"<<typeC<<endl ; 17 }
三、 复制构造函数(拷贝构造函数)-------------用于根据一个已存在的对象复制出一个新的该类对象,一般在函数中会将已经存在的对象的数据成员的值复制一份到新创建的对象中。
1.复制构造函数的引入:
- 同一个类的对象在内存中有完全相同的结构,如果作为一个整体进行复制是完全可能的。这个复制过程只需要复制数据成员,因为函数成员是共用的(在内存中只有一份代码)。在建立对象时可以用同一个类的另一个对象来初始化该对象,这时所用到的构造函数成为复制构造函数。
- 若没有显示的写复制构造函数,系统会默认创建一个复制构造函数但当类中有指针成员时,由系统创建的该复制构造函数就会存在风险。具体请看下边深复制与浅复制。
2.复制构造函数的参数必须采用引用
在C++中按值传递一个参数时,会在函数中重新分配一块内存建立与参数同类型的变量或对象,再把参数的数据成员赋值给新的变量或对象。在建立这个对象时,编译器就会自动为这个对象调用复制构造函数。如果其参数是真实的对象而不是引用,则又会引入新的一轮调用复制构造函数,出现了无穷递归。
3.复制构造函数被调用的时机:
- 当用一个类的对象去初始化该类的另一个对象(或引用)时系统自动调用拷贝构造函数拷贝赋值。
- 若函数的形参为类对象,调用函数时,实参赋值给形参,系统会自动调用复制构造函数
- 当函数的返回值是类对象时,系统自动调用复制构造函数
-
1 对于CGoods类,复制构造函数为: 2 CGoods(CGoods & cgd) 3 { 4 strcpy(Name,cgd.Name); 5 price=cgd.price; 6 amount=cgd.amount; 7 total_value=cgd.toyal_value; 8 } 9 10 11 CGoods Car1("夏利2000",30,98000.0) 12 //调用三个参数的复制构造函数 13 CGoods Car2=Car1;//调用复制构造函数 14 CGoods Car3(Car1); 15 //调用复制构造函数,Car1为实参 16 这三个对象的初始化结果完全一样
示例代码:
-
1 class A 2 { 3 public: 5 A(const A& a){ 6 data=a.data; 7 cout<<"拷贝构造函数调用\n"; 8 } 9 A& operator=(const A&a){ 10 data=a.data; 11 cout<<"调用赋值函数\n"; 12 return *this; 13 } 14 15 int data; 16 }; 17 18 void fun1(A a) 19 { 20 return ; 21 } 22 23 A fun2() 24 { 25 A a; 26 return a; 27 } 28 29 int _tmain(int argc, _TCHAR* argv[]) 30 { 31 A a; 32 A b(a); //用类的一个对象a去初始化另一个对象b 33 A c=a; //用类的一个对象a去初始化另一个对象c,注意这里是初始化,不是赋值 34 fun1(a); //形参为类对象,实参初始化形参,调用拷贝构造函数。 35 A d=fun2(); //函数返回一个类对象时 36 d=a; //d已经初始化过了,这里是赋值,调用赋值函数 37 38 return 0; 39 } 40 //A c=a; d=a;前一个是初始化,后一个是赋值,两者不同
-
四、深复制与浅复制
当对一个对象进行拷贝时,编译系统会自动调用一种构造函数--复制(拷贝)构造函数,如果用户未定义拷贝构造函数则会调用系统默认的拷贝构造函数。
- 在未自定义拷贝构造函数时,在复制对象时调用默认的拷贝构造函数时,进行的时浅拷贝。容易发生内存泄漏
- 深拷贝即调用的自定义的拷贝构造函数,不但对指针进行拷贝,还对指针指向的内容进行拷贝,拷贝后的指针是指向两个不同地址的指针。
1.对类对象进行复制的时候即把对象各数据成员的值原样复制到目标对象中时。当类中涉及到指针类型的数据成员的时候,往往会产生指针悬挂问题。看个简单地例子:
-
1 class A 2 { 3 public: 4 int *a; 5 } 6 A a1; 7 A b1=a1;
b1=a1;执行的是浅复制,此时b1.a与a1.a指向的是同一个内存地址,如果在析构函数里有对内存的释放就会出现内存访问异常。因为一块内存空间被释放了两次。
2.看一个例子:有一个学生类,数据成员为学生的人数和名字
-
1 #include <iostream> 2 using namespace std; 3 4 class Student 5 { 6 private: 7 8 int num; 9 char *name; 10 public: 11 Student(); 12 ~Student(); 13 }; 14 15 Student::Student() 16 { 17 18 name=new char(20); 19 cout<<"Student"<<endl; 20 } 21 22 Studet::~Student() 23 { 24 cout<<"~Student"<<endl; 25 delete name; 26 name=NULL; 27 } 28 29 int main() 30 { 31 {//花括号让s1,s2变成局部对象,方便测试 32 Student s1; 33 Student s2(s1);//复制对象 34 sysem("ause"); 35 return 0; 36 } 37 }
- 执行结果是:调用一次构造函数一次复制构造函数调用两次析构函数。两个对象的指针成员所指向的内存相同。name指针被分配一次内存,但是在程序结束时却被释放了两次,出现错误。
3.所以在对含有指针成员的对象进行拷贝时,必须要自定义拷贝构造函数使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏的发生。
- 下边是添加了自定义拷贝函数的例子:
-
1 #include <iostream> 2 using namespace std; 3 4 class Student 5 { 6 private: 7 int num; 8 char *name; 9 public: 10 Student(); 11 ~Student(); 12 Student(const Student &s);//拷贝构造函数,const防止对象被改变 13 }; 14 15 Student::Student() 16 { 17 name = new char(20); 18 cout << "Student" << endl; 19 20 } 21 Student::~Student() 22 { 23 cout << "~Student " << (int)name << endl; 24 delete name; 25 name = NULL; 26 } 27 Student::Student(const Student &s) 28 { 29 name = new char(20); 30 memcpy(name, s.name, strlen(s.name)); 31 cout << "copy Student" << endl; 32 } 33 34 int main() 35 { 36 {// 花括号让s1和s2变成局部对象,方便测试 37 Student s1; 38 Student s2(s1);// 复制对象 39 } 40 system("pause"); 41 return 0; 42 }
- 执行过程:调用一次构造函数,一次自定义复制构造函数,两次析构函数。两个对象的指针成员所指内存不同
- 浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间。
- 深拷贝不仅对指针进行拷贝还对指针指向的内容进行拷贝,经过深拷贝后的指针是指向两个不同地址的指针。
要考虑自定义拷贝构造函数的情况有以下三种- 复制含指针成员的对象时即用一个类的对象去初始化该类的另一个对象的时候
- 函数的形参为类对象时。调用函数时会有实参到形参的拷贝此时会调用拷贝构造函数
- 当函数的返回为类对象时。在函数和返回时会建立一个和类对象一样的临时类对象变量,并将返回的类对象赋值给新创建的类对象,此时会调用拷贝构造函数
浅拷贝问题的本质在于析构函数释放多次堆内存,使用std::shared_ptr智能指针,可以完美解决这个问题