面向对象先来看看class怎么写。侯捷老师分享的如下两种class分类我觉得很到位:
•Class without pointer member(s)
•Class with pointer member(s)
1. Class without pointer member(s)
这种Class在上一篇《C++编程习惯(C++预备篇)》
中的MyComplex已经介绍了很多注意点。只有友元函数还没有举例说明。
"友元"解决什么问题:
私有成员只能在类的成员函数内部访问,如果想在别处访问对象的私有成员,只能通过类提供的接口(成员函数)间接地进行。这固然能够带来数据隐藏的好处,利于将来程序的扩充,但也会增加程序书写的麻烦。C++ 是从结构化的C语言发展而来的,需要照顾结构化设计程序员的习惯,所以在对私有成员可访问范围的问题上不可限制太死。C++ 设计者认为,如果有的程序员真的非常怕麻烦,就是想在类的成员函数外部直接访问对象的私有成员,那还是做一点妥协以满足他们的愿望为好,这也算是眼前利益和长远利益的折中。因此,C++就有了友元(friend)的概念。
1.1 友元函数
友元函数可以访问类的私有成员。
如下两个例子分别介绍了友元成员函数和友元非成员函数。
// rectangle.hpp
#ifndef __MYRECT__
#define __MYRECT__
class CRectangle
{
public:
CRectangle(int height = 0, int width = 0):mHeight(height), mWidth(width)
{}
int getHeight();
int getWidth();
friend int calculateAreaFriendly(CRectangle& friendRec);// 声明友元函数
private:
int mHeight;
int mWidth;
};
#endif
// rectangle.cpp
#include "rectangle.hpp"
int CRectangle::getHeight()
{
return mHeight;
}
int CRectangle::getWidth()
{
return mWidth;
}
// main.cpp
#include "rectangle.hpp"
#include <iostream>
using namespace std;
// 非友元函数
int CalculateRectangleArea(CRectangle& rect)
{
return rect.getHeight() * rect.getWidth(); // 非友元函数只能通过类的成员函数去访问私有成员
}
// 友元函数 (非成员函数)
int calculateAreaFriendly(CRectangle& friendRec)
{
return friendRec.mWidth * friendRec.mHeight; // 友元函数可以直接去访问私有成员
}
int main()
{
CRectangle rt(100, 100);
cout << "result = " << CalculateRectangleArea(rt) << endl;
CRectangle rt_friend(1000, 1000);
cout << "result = " << CalculateRectangleArea(rt_friend) << endl;
}
Gen-2:~/Documents/Projects/C++/OopDemo/frienddemo$ g++ main.cpp src/rectangle.cpp -Iinclude -o main
Gen-2:~/Documents/Projects/C++/OopDemo/frienddemo$ ./main
result = 10000
result = 1000000
#include <iostream>
using namespace std;
class CItem; // 前向声明。也会被用作解决a头文件相互依赖
class CList
{
public:
void print(CItem& citem);
};
class CItem
{
friend void CList::print(CItem& citem); // 友元函数 (成员函数)
public:
CItem(int i):index(i)
{}
private:
int index;
inline void printDetails()
{
cout << "CItem printDetails" << endl;
}
};
void CList::print(CItem& citem)
{
cout << "CItem index = " << citem.index << endl;
citem.printDetails();
}
int main()
{
CItem citem(8);
CList clist;
clist.print(citem);
}
CItem index = 8
CItem printDetails
1.2 友元类
对于类的私有方法,只有在该类中允许访问,其他类是不能访问的,但在开发程序时,如果两个类的耦合度比较紧密,能够在一个类中访问另一个类的私有成员会带来很大的方便。
#include <iostream>
using namespace std;
class CItem;
class CList
{
public:
void Print(CItem& citem);
};
class CItem
{
friend class CList; // 定义CList为CItem的友元类
public:
CItem(int i):index_(i)
{
cout << " CItem construct, index_ = " << index_ << endl;
}
private:
int index_;
inline void PrintDetails()
{
cout << "CItem printDetails" << endl;
}
};
void CList::Print(CItem& citem)
{
citem.PrintDetails(); // CList作为CItem的友元类,可以访问CItem的私有函数
}
int main()
{
CItem citem(20);
CList clist;
clist.Print(citem);
return 0;
}
chris@chris-virtual-machine:~/Documents/Projects/C++/OopDemo1$ ./main
CItem construct, index_ = 20
CItem printDetails
2. Class with pointer member(s)
与Class without pointer member(s)最大的区别在于,这种Class是一定要自己写 拷贝构造函数和拷贝赋值函数的(深拷贝),不能使用编译器默认的拷贝构造和拷贝赋值。当然如果涉及到new内存,可能还需要自己写析构函数。
// MyString.h
#ifndef __MYSTRING__
#define __MYSTRING__
#include <iostream>
class MyString
{
public:
// 拷贝构造
MyString(const char* cstr = 0);
MyString(const MyString& str);
// 拷贝赋值
MyString& operator = (const MyString& str);
// 析构函数
~MyString();
private:
char* m_data;
};
#endif
// MyString.cpp
#include <iostream>
#include "MyString.h"
using namespace std;
// 拷贝构造
inline MyString::MyString(const char* cstr = 0)
{
cout << "拷贝构造MyString(const char* cstr = 0)" << endl;
if (cstr)
{
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}
else
{ // 未指定初值
m_data = new char[1];
*m_data = '\0';
}
}
inline MyString::MyString(const MyString& str)
{
cout << "拷贝构造MyString(const MyString& str)" << endl;
// 在C++里对象没有空不空,
// 只有指针有空不空的概念(例如上一个构造函数),指针用NULL来判断
m_data = new char[strlen(str.m_data) + 1];
strcpy(m_data, str.m_data);
}
// 拷贝赋值
inline MyString& MyString::operator = (const MyString& str)
{
cout << "拷贝赋值MyString(const MyString& str)" << endl;
// 检查自我赋值,即检查赋值号左右两边是不是相同的。作用如下:
// (1) 提升效率:如果赋值符号两边是同一个,则不需要做后面的操作了;
// (2) 避免产生不确定的行为出错:如果赋值符号两边是同一个且这里不直接return的话,接下来会去delete[] mdata,这时候由于赋值号右边和左边是同一块地址,即相当于也delete了str的m_data,紧接着去访问str就可能出现不确定的行为。
if (this == &str)
{
return *this;
}
delete[] m_data;
m_data = new char[strlen(str.m_data)+1];
strcpy(m_data, str.m_data);
return *this;
}
// 析构函数
inline MyString::~MyString()
{
cout << "析构函数" << endl;
}
int main()
{
MyString str1("hello world");// 拷贝构造MyString(const char* cstr = 0)
MyString str2 = str1;// 【注意这里是拷贝构造】拷贝构造MyString(const MyString& str)
MyString str3(str1);// 拷贝构造MyString(const MyString& str)
MyString str0; // 拷贝构造MyString(const char* cstr = 0)
str0 = str1;// 【注意这里是拷贝赋值】拷贝赋值MyString(const MyString& str)
return 0;
}
2.1 拷贝构造
如下三种情况下会调⽤拷⻉构造函数
- ⼀个对象以值传递的⽅式传⼊函数体,需要拷⻉构造函数创建⼀个临时对象压⼊到栈空间中。
void ConFun(const MyString& t1)
{
MyString t2 = t1;// // 这里会调用拷调用拷贝构造MyString(const MyString& str)
}
- ⼀个对象以值传递的⽅式从函数返回,需要执⾏拷⻉构造函数创建⼀个临时对象作为返回值。
MyString ConFun()
{
MyString a;
return a;
}
MyString B = ConFun();// 这里会调用拷调用拷贝构造MyString(const MyString& str)
- 定义新对象,并用已有对象初始化新对象。
MyString t2 = t1;// 拷贝构造
或者是
MyString t2(t1);// 拷贝构造
拷⻉构造函数必须是引⽤传递,不能是值传递。
原因:为了防⽌递归调⽤。当⼀个对象需要以值⽅式进⾏传递时,编译器会⽣成代码调⽤它的拷⻉构造函数⽣成⼀个副本,如果类 A 的拷⻉构造函数的参数不是引⽤传递,⽽是采⽤值传递,那么就⼜需要为了创建传递给拷⻉构造函数的参数的临时对象,⽽⼜⼀次调⽤类 A 的拷⻉构造函数,这就是⼀个⽆限递归。
2.2 拷贝赋值
拷贝赋值即“=操作符重载”:除了以上三种拷贝构造函数的情况,其余将一个对象赋值给另外一个对象都会调用赋值操作符重载。
MyString t1;
MyString t2;
t2 = t1;// 操作符重载