面向对象的设计三原则:封装,继承,多态
类的继承特性
class A
{
public:
void Func1(void);
void Func2(void);
};
class B
{
public:
void Func3(void);
void Func4(void);
};
int main()
{
B b;
b.Func1();
b.Func2();
return 0;
}
C++的“继承”特性可以提高程序的可复用性。表现在派生类可以调用基类的函数。
构造函数
除了名字之外 ,构造函数和析构函数还有另外一个特别之处就是没有返回值类型,这和返回值是void的函数不同。
初始化列表
我们一般习惯在构造函数体内初始化数据成员,然而这个不是真正的初始化,而是赋值。不过,由于构造函数是创建一个对象时自动调用的第一个成员函数,因此我们也愿意把构造函数体内的赋值语句当做初始化来看待。
class A{
public:
A(int x);
};
class B:public A{
public:
B(int x,int y);
};
//在初始化列表里调用A的构造函数
B::B(int x,int y):A(x)
{
}
类的非静态const数据成员和引用成员只能在初始化列表里初始化,因为它们只存在初始化语义,而不存在赋值语义。
class A{
A(void);
A(const A& other);
A& operator=(const A& other);
};
class B{
public:
B(const A& a);
private:
A m_a;
};
//(1)采用初始化列表的方式初始化
B::B(const A& a):m_a(a)
{
}
//(2)采用函数体内赋值的方式进行初始化
B::B(const A& a)
{
m_a = a;
}
看着差不多,其实二者的效率不同,第一种方式,类B的构造函数在其初始化列表里调用了A的拷贝构造函数,第二种方式,类B的构造函数在函数体内用赋值的方式,我们只是看到一个赋值语句,但是实际上B的构造函数干了两件事,先暗地创建m_a的对象(调用A的默认构造函数),再调用类A的赋值函数,才将参数a传给m_a。而对于内部数据类型,而这其实差不多。
注意:
当使用成员初始化列表来初始化数据成员的时候,这些成员真正的初始化顺序并不一定和你在初始化列表中安排的顺序一致,编译器总是按照他们在类中声明的次序进行初始化。
单个参数的构造函数如果不添加explicit关键字,会定义一个隐含的类型转换,添加explicit关键字会消除这种隐式的转换。
class A
{
public:
A(A copy){...}//(1)
A(const A& copy){...}//(2)
};
A a;
A b = a;
注意:
1) A b =a调用的是拷贝构造函数,而不是赋值操作符。赋值操作符是赋值语义不是初始化语义。
2)实际上C++中是不允许出现(1)这种形式的拷贝构造函数,即它是非法的。原因如下:这个拷贝构造函数在参数传递的过程中又需要调用拷贝构造函数,无限的递归中。
String类
class String
{
public:
String(const char* str="");//默认构造函数
String(const String& copy);
~String();
String& operator=(const String& assign);
private:
size_t m_size;
char* m_data;
};
String::String(const char* str)
{
if(NULL == str)
{
m_data = new char[1];
*m_data='\0';
m_size = 0;
}
else
{
int size = strlen(str)+1;
m_data = new char[size];
strcpy(m_data,str);
m_size =size;
}
}
String::~String()
{
delete []m_data;
}
这里要说下拷贝构造函数和赋值操作符,如果不主动定义拷贝构造函数和赋值操作符,编译器会按以”按成员拷贝“的方式自动生成相应的默认函数,倘若类中含有指针成员和引用成员,那么着两个默认的函数就可能有隐含的错误。
以类String的两个对象a和b为例,假设a.m_data的内容为"hello",b.m_data的内容为"world",现将a赋值给b,默认的赋值操作符”按成员拷贝“意味着执行b.m_data=a.m_data。这就会造成三个错误。
1) b.m_data原先持有的内存没有释放就被覆盖了,造成内存泄露
2)b.m_data和a.m_data指向了同一块内存,a,b任何一方的改变都对另外一个造成改变
3)在对象被析构的时候,m_data被delete两次。
String::String(const String& other)
{
//不需要对引用做非空判断,这个也是和指针的重要区别之一
//注意,这里允许访问other的私有数据成员
int size = strlen(other.m_data);
m_data = new char[size+1];
strcpy(m_data,other.m_data);
m_size = size;
}
拷贝构造函数的实现很类似于默认构造函数,只是不需要判断指针为空的情况,这点与赋值操作符是完全不同的。
String& String::operator=(const String& str)
{
if(this != &str)
{
int size = strlen(str.m_data);
char* tmp = new char[size+1];
strcpy(tmp,str.m_data);
delete []m_data;
m_data = tmp;
m_size = size;
}
return *this;
}
用偷懒的办法处理拷贝构造函数和赋值操作符
如果我们是在不想编写拷贝构造函数和赋值操作符(不想拷贝对象),又不允许别人使用编译器自动生成的默认函数时该怎么办。偷懒的办法是:只需要将拷贝构造函数和赋值操作符声明为private。并且不实现他们,但是不能像纯虚函数那样设置为0。甚至可以把类的所有构造函数和赋值函数都声明为private,这样就彻底阻止了该类的实例化了。或者把默认构造函数声明为private,而把其他带参数的构造函数声明为public,这样就迫使用户使用带参数的构造函数。
String& String::operator=(const String& str)
{
if(this != &str)
{
int size = strlen(str.m_data);
char* tmp;
tmp = new char[size+1];
strcpy(tmp,str.m_data);
delete []m_data;
m_data = tmp;
m_size = size;
}
return *this;
}