1.写一个继承类的复制构造函数
(对继承类的复制构造函数的理解)
如果基类中没有私有成员,即所有成员都能被派生类访问,则派生类的复制构造函数可以很容易写。但如果基类有私有成员,并且这些私有成员必须在调用派生类的复制构造函数时被初始化,在这种情况下该如何解决?
编写继承类的复制函数有一个原则:使用基类的复制构造函数。这个原则就是解决上述问题的方法。
#include<iostream>
using namespace std;
class Base
{
public:
Base():i(0){cout<<"Base()"<<endl;}
Base(int n):i(n){cout<<"Base(int)"<<endl;}
Base(const Base &b):(b.i)
{
cout<<"Base(Base&)"<<endl;
}
private:
int i;
};
class Derived:public Base
{
public:
Derived():Base(0),j(0){cout<<"Derived()"<<endl;}
Derived(int m,int n):Base(m),j(n){cout<<"Derived(int)"<<endl;}
Derived(Derived &obj):Base(obj),j(obj.j)
{
cout<<"Derived(Derived&)"<<endl;
}
private:
int j;
};
int main()
{
Base b(1);
Derived obj(2,3);
cout<<"______”<<endl;
Derived d(obj);
cout<<"_____________"<<endl;
return 0;
}
Derived类继承自Base类,因此在Derived类内不能使用obj.i或Base::i的方式访问Base的私有成员i。很明显,其复制构造函数只有使用Base(obj)代码22行的方式调用其基类的复制构造函数来给基类的私有成员i初始化。
2.复制构造函数与赋值函数有什么区别
- 1)复制构造函数是一个对象来初始化一块内存域,这块内存就是新对象的内存区。
eg:
class A;
A a;
A b=a;//复制构造函数调用
A b(a);//复制构造函数调用
而赋值函数是对于一个已经被初始化的对象来进行operator=操作。例如:
class A
A a;
A b;
b=a;//赋值函数调用
- 2)一般来说是在数据成员包含指针对象的时候,应付两种不同的处理需求:一种是复制指针对象,一种是引用只针对象。复制构造函数在大多数情况下是复制,赋值函数则是引用对象。
- 3)实现不一样。复制构造函数首先是一个构造函数,它调用的时候是通过参数传进来的那个对象来初始化来初始化产生一个对象。赋值函数则是把一个对象赋值给一个原有的对象,所以,如果原来的对象中有内存分配,要先把内存释放掉,而且还要检查一下两个对象是不是同一个对象,如果是的话,就不做任何操作。
3.编写类String的构造函数,析构函数和赋值函数
已知类String的原型为:
class String
{
public:
String(const char *str=NULL);//普通构造函数
String(const String &other);//赋值构造函数
~String(void);//析构函数
String & operator=(const String &other);//赋值函数
private:
char *m_String;//私有成员,保存字符串
};
程序代码如下:
#include<iostream>
using namespace std;
class String
{
public:
String(const char *str=NULL);//普通构造函数
String(const String &other);//赋值构造函数
~String(void);//析构函数
String & operator=(const String &other);//赋值函数
private:
char *m_String;//私有成员,保存字符串
};
String::~String(void)
{
cout<<"Destructing"<<endl;
if(m_String!=NULL)//如果m_String不为NULL,释放堆内存
{
delete [] m_String;
m_String=NULL;//释放后置为NULL
}
}
String::String(const char *str)
{
cout<<"Construcing"<<endl;
if(str==NULL)
{
m_String=new char[1];//分配一个字节
*m_String='\0';//将之赋值为字符串结束符
}
else
{
m_String=new char[strlen(str)+1];//分配空间容纳str内容
strcpy(m_String,str);//赋值str到私有成员
}
}
String::String(const String &other)
{
cout<<"Constructing Copy"<<endl;
m_String=new char[strlen(other.m_String)+1];//分配空间容纳str内容
strcpy(m_String,other.m_String);//复制str到私有成员
}
String & String::operator=(const String &other)
{
cout<<"Operate=Function"<<endl;
if(this==&other) //如果对象与other是同一个对象
{
return *this; //直接返回本身
}
delete [] m_String; //释放堆内存
m_String=new char[strlen(other.m_String)+1];
strcpy(m_String,other.m_String);
return *this;
}
int main()
{
String a("hello");//调用普通构造函数
String b("world");//调用普通构造函数
String c(a);//调用复制构造函数
c=b;//调用赋值函数
return 0;
}
1)普通构造函数:这里判断了传入的参数是否为NULL。如果是NULL,初始化一个字节的空字符串(包括结束符’\0’);如果不是,分配足够大小长度的堆内存来保存字符串。
2)复制构造函数:只是分配足够小长度的堆内存来保存字符串。
3)析构函数:如果类私有成员m_String不为NULL,释放m_String指向的堆内存,并且为了避免产生野指针,将m_String赋为NULL。
4)赋值函数:首先判断当前对象与引用传递对象是否是同一个对象,如果是,不做操作,直接返回;否则,先释放当前对象的堆内存,然后分配足够大小长度的堆内存复制字符串。
4.了解C++类各成员函数的关系
(构造函数,析构函数和赋值函数之间的关系)
#include<iostream.h>
class A
{
private:
int num;
public:
A()
{
cout<<"Default constructor"<<endl;
}
~A()
{
cout<<"DEsconstructor"<<endl;
cout<<num<<endl;
}
A(const A &a)
{
cout<<"Copy constructor"<<endl;
}
void operator=(const A &a){
cout<<"Overload operator"<<endl;
}
void SetNum(int n){
num=n;
}
};
void main()
{
A a1;
A a2(a1);
A a3=a1;
A &a4=a1;
a1.SetNum(1);
a2.SetNum(2);
a3.SetNum(3);
a4.SetNum(4);
}
代码第31行,定义了一个对象a1,调用的是默认的构造函数。
嗲吗第32行,用a1初始化一个对象a2,调用的是复制构造函数。
代码第33行,同上。注意,这里不是调用赋值函数,这里属于对象a3的初始化,而不是赋值。若调用赋值,则·
A a3;
a3=a1;
代码第34行,定义a4为a1的一个引用,不调用构造函数或赋值函数。
代码第35到38行,调用各个对象的SetNum()成员函数为私有成员num赋值。这里注意,由于a4为a1的引用,因此a4.SetNum()实际上和a1.SetNum()等同。
当main()函数退出时,对象析构函数与调用构造函数顺序相反,依次为a3,a2,a1.
5.C++类的临时对象
(构造函数,析构函数和赋值函数的编写方法)
已知class B以及Play()函数定义如下:
#include<iostream.h>
class B
{
public:
B()
{
cout<<"default constructor"<<endl;
}
~B()
{
cout<<"destructed"<<endl;
}
B(int i):data(i)//初始化私有成员data
{
cout<<"constructed by parameter"<<data<<endl;
}
private:
int data;
};
B play(B b)
{
return b;
}
分析下面两个main函数的输出。
第一个main函数:
int main(int argc,char* argv[])
{
B t1=Play(5);
B t2=Play(t1);
return 0;
}
第二个main函数:
int main(int argc,char* argv[])
{
B t1=Play(5);
B t2=Play(10);
return 0;
}
这里调用Play()函数时,有两种参数类型的传递方式。
如果传递的参数是整数型,那么在其函数栈中首先会调用带参数的构造函数,产生一个临时对象,然后返回前(在return代码执行时)调用类的复制构造函数,生成临时对象(这样函数返回后主函数中的对象就被初始化了),最后这个临时对象会在函数返回时(在return代码执行后)析构。
如果传递参数是B类的对象,那么只有第一步与上面的不同,就是其函数栈中会首先调用复制构造函数产生一个临时对象,其余步骤完全相同。
可以看出,两种情况的区别是采用了不同的方法生成临时对象(一个是调用带参数的构造函数,另一个是调用复制构造函数)。
在第一个main函数中,对象t1使用了传入整型数的方式调用Play()函数,而对象t2使用了传入B的对象的方式调用Play()函数。在第二个main()函数中,对象t1和t2都使用了传入整型数的方式调用Play()函数。第一个main()函数下的执行结果为:
在第二个main()函数下的执行结果为:
6.复制构造函数和析构函数
(对复制构造函数和析构函数的理解)
#include<iostream.h>
class A
{
public:
A()
{
cout<<"This is A Construction"<<endl;
}
virtual ~A()
{
cout<<"This is A destruction"<<endl;
}
};
A fun()
{
A a;
return a;
}
void main()
{
{
A a;
a=fun();
}
}
不是说构造函数和析构函数时成对的吗?为什么少了一个构造函数呢?
因为:
构造函数和析构函数确实是成对的。构造函数除了普通构造函数之外,还包括复制构造函数。
上面的程序中一共构造了三个对象,分别是main()函数中的a(代码第24行),fun()函数中的a(代码第17行)以及fun返回时生成的临时对象(代码第18行)。前两个对象都是用普通构造函数构造的,而由fun返回时生成的临时对象是由复制构造函数生成的。上面的程序中只是在普通构造函数中打印信息。加入自定义复制构造函数和赋值函数,如下所示:
A(A &a)
{
cout<<"This is A Copy Construction"<<endl;
}
A& operator=(const A &a)
{
cout<<"This is an assignment function"<<endl;
return *this;
}
执行结果如下:
可以看出,此时的构造函数和析构函数都被执行了3次。另外,在main()函数中把fun()返回的对象赋给了对象a,此时会调用赋值函数。
构造函数和析构函数时成对出现的,原始程序中的fun返回时生成的临时对象是由复制构造函数生成的。这里没有在复制构造函数中输出信息(编译器生成默认的复制构造函数),所以看上去构造函数比析构函数少了一个。
7.看代码写结果——C++静态成员和临时对象
(对C++静态成员和临时对象的理解)
#include<iostream>
using namespace std;
class human
{
public:
human()
{
human_num++;
}
static int human_num;
~human()
{
human_num-
print();
}
void print()
{
cout<<"human num is:"<<human_num<<endl;
}
};
int human::human_num=0;
human f1(human x)
{
x.print();
return x;
}
int main(int argc,char* argv[])
{
human h1;
h1.print();
human h2=f1(h1);
h2.print();
return 0;
}
解析:
这个程序的human类有一个静态成员human_num,每执行一次,普通构造函数human_num加1,每执行一次,析构函数human_num减1.注意,在f1()函数中会使用默认的复制构造函数,而默认的复制构造函数没有对human_num处理。
代码第34行,只构造了对象h1(调用普通构造函数),因此打印1.
代码第35行使用值传递参数的方式调用了f1()函数,这里分为3步:
1)在f1()函数内首先会调用复制构造函数生成一个临时对象,因此代码第27行打印1。
2)f1()函数内调用复制构造函数,给main的对象h2初始化(复制临时对象)。
3)f1()函数返回后,临时对象发生析构,此时human的静态成员human_num为0,打印出0.
代码第36行打印的还是0.
main()函数结束时有h1和h2两个对象要发生析构,所以分别打印出-1和-2。
程序的意图其实很明显,就是静态成员用human_num记录类human的实例数,然而,由于默认的复制构造函数没有对静态数据成员操作,导致了执行结果的不正确。这里可以通过添加一个自定义的复制构造函数解决。
human(human &h)
{
human_num++;
}
此时human_num就能起到相应的作用了。
执行结果如下:
1 1
2 1
3 0
4 0
5 -1
6 -2
8.什么是临时对象?临时对象在什么情况下产生
(对C++临时对象的理解)
当程序员之间进行交谈时,经常把仅仅需要一小段时间的变量成为临时变量。例如在下面的swap函数中:
void swap(int &a,int &b)
{
int temp=a;
a=b;
b=temp;
}
通常称temp为临时变量。但是在C++里,temp根本不是临时变量。实际上,它只是一个函数的局部变量。
真正的临时对象是看不见的,它不会出现在程序代码中。大多数情况下,它会影响程序执行的效率,所以有时想避免临时对象的产生。它通常在以下两种情况下产生。
1)参数按值传递
2)返回值按值传递
代码如下:
#include<iostream>
using namespace std;
class Test
{
public:
Test():num(0){}//默认构造函数
Test(int number):num(number){}//带参数的构造函数
void print()
{
cout<<"num="<<num<<endl;
}
~Test()//析构函数,打印this指针和私有成员num
{
cout<<"destructor:this="<<this<<",num="<<num<<endl;
}
private:
int num;
};
void fun1(Test test)//参数按值传递
{
test.print();
}
Test fun2()
{
Test t(3);
return t;//返回值按值传递
}
int main(int argc,char* argv[])
{
Test t1(1);
fun1(t1);//对象传入
fun1(2);//整型数2传入
t1=fun2();
return 0;
}
程序中的fun1()函数的参数是按值传递的,fun2()函数的返回值是按值传递的。在Test类的析构函数中打印了this指针的值,下面是程序的执行结果。
这里代码第36行和第37行使用了两种类型的参数传入func1()函数。它们都会产生临时变量,不同点是要采用不同的方式;第36行的调用使用了复制构造函数创建临时变量,而第37行调用使用的则是带参数的构造函数创建临时变量。
如何避免临时变量的产生呢?可以使用按引用传递代替按值传递。例如把上面的fun1()函数改成如下形式:
void fun1(Test &test)
{
test.print();
}
这样,fun1()函数的参数就是一个已经存在的对象引用,此时整数型是不能传进来的。执行下面的主程序。
int main(int argc,char* argv[])
{
Test t1(1);
fun1(t1);//对象引用传入
return 0;
}
程序执行如下:
可以看到,此时就不会产生临时变量了。
注意:引用必须有一个实在的,可引用的对象,否则引用时错误的。因此,在没有实在的,可引用的对象的时候,只有依赖于临时对象。