目录
1 概述
所谓封装,就是将某些东西包装和隐藏起来,让外界无法直接使用,只能通过某些特定的方式才能访问。面向对象将万物都视为“对象”,任何对象都具有特性和行为。我们将其特性称为“成员变量”,将其行为称为“成员函数”,被封装的特性只能通过特定的行为去访问。
2 封装
C++封装通过类(class)来实现。类定义可以使用关键字class和struct.
2.1 class or struct
- class定义类默认成员变量是私有(private)的,可以定义公共(public)成员函数对其进行操作
- struct 定义类默认成员变量是公有的。
例如:
#include <iostream>
class Class1
{
int a; //a是私有的
public:
int get_a() { return a; }
bool set_a(int v)
{
if(v > MAX_A)
return false;
a = v;
return true;
}
static int const MAX_A = 100;
};
struct Class2
{
double get_b() const { return b; }
float get_c() const { return c; }
int a;//a是公有的
private:
double b;//b是私有的
float c;//c是私有的
};
int main(int argc, char *argv[])
{
Class1 class1;
Class2 class2;
std::cout << "class1.a: " << class1.a << std::endl;//该行代码编译会出错
std::cout << "class1.a: " << class1.get_a() << std::endl;
std::cout << "class2.a: " << class2.a << std::endl;
return 0;
}
编译结果:
test_001.cpp:16:25: error: ‘int Class1::a’ is private within this context
16 | std::cout << class1.a << std::endl;
| ^
test_001.cpp:5:9: note: declared private here
5 | int a; //a是私有的
注释出错代码
int main(int argc, char *argv[])
{
Class1 class1;
Class2 class2;
//std::cout << "class1.a: " << class1.a << std::endl;//该行代码编译会出错
std::cout << "class1.a: " << class1.get_a() << std::endl;
std::cout << "class2.a: " << class2.a << std::endl;
std::cout << "class2.b: " << class2.get_b() << std::endl;
std::cout << "class2.c: " << class2.get_b() << std::endl;
}
运行结果:
class1.a: 32512
class2.a: -1067117848
class2.b: 6.89919e-310
class2.c: 0
结果分析:由于类的成员变量没有初始化,所以打印出的是随机数。C++类构造函数可以完成成员变量初始化。
2.2 构造/析构函数
C++构造函数名称与类名相同,析构函数名是~+类名
为前面的类增加构造和析构函数:
class Class1
{
int a; //a是私有的
public:
Class1()
: a(0)
{
std::cout << "Class1() is called" << std::endl;
}
Class1(int v)
: a(v)
{
std::cout << "Class1(int) is called" << std::endl;
}
~Class1()
{
std::cout << "~Class1() is called" << std::endl;
}
int get_a() { return a; }
};
struct Class2
{
Class2()
: a(1)
, b(2.0)
, c(3.0)
{
std::cout << "Class2() is called" << std::endl;
}
~Class2()
{
std::cout << "~Class2() is called" << std::endl;
}
double get_b() const { return b; }
float get_c() const { return c; }
int a;//a是公有的
private:
double b;//b是私有的
float c;//c是私有的
};
int main(int argc, char *argv[])
{
Class1 class1_0;
Class1 class1_1(5);
Class2 class2_0;
Class2 class2_1(4, 6.0, 7.0);
//std::cout << "class1.a: " << class1.a << std::endl;//该行代码编译会出错
std::cout << "class1_0.a: " << class1_0.get_a() << std::endl;
std::cout << "class1_1.a: " << class1_1.get_a() << std::endl;
std::cout << "class2_0.a: " << class2_0.a << std::endl;
std::cout << "class2_0.b: " << class2_0.get_b() << std::endl;
std::cout << "class2_0.c: " << class2_0.get_c() << std::endl;
std::cout << "class2_1.a: " << class2_1.a << std::endl;
std::cout << "class2_1.b: " << class2_1.get_b() << std::endl;
std::cout << "class2_1.c: " << class2_1.get_c() << std::endl;
return 0;
}
上面构造函数分别定义两个,带采数和不带参数,这就是C++的函数重载(函数名称相同,参数不同).
运行结果
Class1() is called
Class1(int) is called
Class2() is called
Class2(int, double, float) is called
class1_0.a: 0
class1_1.a: 5
class2_0.a: 1
class2_0.b: 2
class2_0.c: 3
class2_1.a: 4
class2_1.b: 6
class2_1.c: 7
~Class2() is called
~Class2() is called
~Class1() is called
~Class1() is called
总结: C++类构造函数和析构函数是自动调用。可以在构造函数中做变量以及资源初始化,在析构函数中进行资源释放。在多线程编程中还可以在构造函数中加锁,在析构函数中解锁。
2.3 拷贝构造/赋值运算符
为Class1增加拷贝构造和赋值运算符:
class Class1
{
int a; //a是私有的
public:
Class1()
: a(0)
{
std::cout << "Class1() is called" << std::endl;
}
Class1(Class1 const& r)
: a(r.a)
{
std::cout << "Class1(Class1 const&) is called" << std::endl;
}
Class1& operator=(Class1 const& r)
{
if(this != &r)
a = r.a;
std::cout << "operator=(Class1 const&) is called" << std::endl;
return *this;
}
};
void main(int argc, char *argv[])
{
Class1 class1_1(5);
Class1 clsss1_2 = class1_1;//拷贝构造
Class1 clsss1_3;
Class2 class2_1(4, 6.0, 7.0);
Class2 clsss2_2 = class2_1;//拷贝构造
Class2 clsss2_3;
clsss1_3 = class1_1;//赋值操作
clsss2_3 = class2_1;//赋值操作
std::cout << "class1_1.a: " << class1_1.get_a() << std::endl;
std::cout << "clsss1_2.a: " << clsss1_2.get_a() << std::endl;
std::cout << "clsss1_3.a: " << clsss1_3.get_a() << std::endl;
std::cout << "class2_1.a: " << class2_1.a << std::endl;
std::cout << "class2_2.a: " << clsss2_2.a << std::endl;
std::cout << "class2_3.a: " << clsss2_3.a << std::endl;
}
运行结果
Class1(int) is called
Class1(Class1 const&) is called
Class1() is called
Class2(int, double, float) is called
Class2() is called
operator=(Class1 const&) is called
class1_1.a: 5
clsss1_2.a: 5
clsss1_3.a: 5
class2_1.a: 4
class2_2.a: 4
class2_3.a: 4
~Class2() is called
~Class2() is called
~Class2() is called
~Class1() is called
~Class1() is called
~Class1() is called
从运行结果Class1的拷贝构造和赋值操做都调用了。是不是很奇怪,我们没有为Class2定义拷贝构造函数和赋值操作,Class2的拷贝构造和赋值操作也可以用,并且结果也正确。其实默认情况下编译会自动生成一个拷贝构造函数。
拷贝分深拷贝与浅拷贝:
- 如果类的成员中有指针,在拷贝时只拷贝指针变量的值,这种叫浅拷贝
- 深拷贝是拷贝指针所指向的数据。
注意: 默认拷贝构造函数只能完成浅拷贝。
拷贝构造重写建议:
- 如果是浅拷贝不需要重写,使用编译自动生成即可。
- 如果是深拷贝则需要重写。
拷贝构造函数使用场景:
- 使用旧对象给新对象初始化时,例如Class1 clsss1_2 = class1_1;
- 使用对象作为函数的参数或返回值时。
赋值运算符与拷贝构造函数一样,默认也是浅拷贝,当需要深拷贝时,需要重写赋值运算符。
当需要重写拷贝构造函数时,一般也需要重写赋值运算符,同时也需要实现析构函数。
这就是C++著名的"大三法则"。
拷贝函数和赋值函数使用建议:
- 拷贝构造和赋值函数不光会赋值本类的数据,也会调用父类和成员类的拷贝构造和赋值函数,而不是单纯的内存拷贝,因此尽量少使用指针成员
- 函数的参数中尽量使用指针和引用,减少调用拷贝构造的次数,这样也可以提高传递的效率。
- 如果由于特殊原因无法实现拷贝构造和赋值函数,可以把只声明不实现然后私有化,防止误用。
- 使用时遵循“大三法则”。
2.4 大三法则
C++中的大三法则是类的拷贝构造函数,赋值运算符和析构函数这三个函数只要一个出现,其它两个也要出现。
举个例子:
class Array
{
int32_t *array_;
uint32_t size_;
public:
Array(uint32_t n)
: array_(new int32_t[n])
, size_(n)
{}
~Array()
{
delete []array_;
}
uint32_t size() const { return size_; }
bool setValue(uint32_t index, int32_t value)
{
if(index >= size_)
return false;
array_[index] = value;
return true;
}
bool getValue(uint32_t index, int32_t& value)
{
if(index >= size_)
return false;
value = array_[index];
return true;
}
};
类定义这里到工作还远没有结束,如果调用拷贝构造函数和赋值操作时,将会导致内存多次释放。
- 一种解决方式,禁止调用调用拷贝构造函数和赋值操作。如下所示:
class Array
{
int32_t *array_;
uint32_t size_;
Array(Array const&);
Array& operator=(Array const&);
public:
//
};
- 另一种解决方式,就是重写拷贝构造函数和赋值操作。
如下所示:
class Array
{
int32_t *array_;
uint32_t size_;
public:
Array(Array const& r)
: size_(r.size_)
, array_(new int32_t[size_])
{
if(array_)
memcpy(array_, r.array_, size_ * sizeof(int32_t));
}
Array& operator=(Array const& r)
{
if(this == &r)
return *this;
if(array_)
delete []array_;
size_ = r.size_;
array_ = new int32_t[size_];
if(array_)
memcpy(array_, r.array_, size_ * sizeof(int32_t));
return *this;
}
};
2.5 静态成员
2.5.1 静态成员变量
类的成员变量可以被static修饰,存储位置由原来的栈或堆变成全局存储,整个程序中只存在一份,被所有的对象共享(静态成员属于类,而不是某个对象)。
静态成员变量用法:
- 静态成员变量在类中声明,但必须在类外初始化,需要加类名:: 表示它属于哪个类,但不需要再加static。
- 静态成员变量虽然在类中定义,但它依然受访问控制符的限制,私有成员和保护成员只能在类内访问。
- 静态成员变量可以当作类范围内的"全局变量使用"。
下面例子通过类成员变量ObjectCount记录Object的对象计数。
struct Object
{
Object()
: value(0)
{
ObjectCount++;
}
Object(Object const& obj)
: value(obj.value)
{
ObjectCount++;
}
~Object()
{
ObjectCount--;
}
Object& operator=(bject const& obj)
{
if(this != &obj)
value = obj.value;
return *this;
}
static int ObjectCount;
int value;
};
int Object::ObjectCount = 0;
2.5.2 静态成员函数
静态成员函数也可以被static修饰,这种成员参数中就没有隐藏this指针,因此静态成员函数不需要对象就可以访问,调用方式是类名::静态成员函数名,如果有对象指针,静态成员函数可以访问私有和保护成员变量,也可调用私有和保护成员函数。
静态成员函数用法:
- 普通成员函数中可以直接访问类的静态成员变量和静态成员函数。
- 而静态成员函数可以作为其它函数的回调函数,或者当作类的接口,实现对类的管理。
用法1:用作单例
struct App
{
static App* Instance()
{
static App theApp;
return &theApp;
}
void name(std::string const& n) { name_ = n; }
void version(std::string const& v) { version_ = v; }
std::string name() const { return name_; }
std::string version() const { return version_; }
private:
App() {}
App(App const&);
App& operator=(App const&);
std::string name_;
std::string version_;
};
#define theApp (App::Instance())
用法2: 回调函数
typedef struct ThreadImp* ThreadHandle;
class Thread
{
ThreadHandle hanlde_;
std::string name_;
int count_;
Thread(Thread const&);
Thread& operator=(Thread const&);
public:
Thread(std::string const& name, int count = 1)
: hanlde_(0)
, name_(name)
, count_(count)
{
}
~Thread();
static int ThreadHandler(Thread *);
void start();
int waitForTerm();
protected:
int run();
};
extern "C" void *thread_run(void *obj)
{
long r = Thread::ThreadHandler(reinterpret_cast<Thread *>(obj));
return reinterpret_cast<void *>(r);
}
int Thread::ThreadHandler(Thread *thread)
{
return thread->run();
}
void Thread::start()
{
hanlde_ = new ThreadImp;
pthread_create(&hanlde_->thread, 0, thread_run, this);
}
int Thread::waitForTerm()
{
long r = -1;
if(hanlde_)
{
void* returnValue;
pthread_join(hanlde_->thread, &returnValue);
r = reinterpret_cast<long>(returnValue);
}
return r;
}
3 友元
友元,可以使普通函数或其他类中的成员函数可以访问某个类的私有成员和保护成员。
3.1 友元函数
友元函数不是类的成员函数, 但是可以访问类的private私有成员和protected保护成员;
友元函数可以是:
- 全局函数
- 成员函数,包括本类和其它类成员函数
例如通过友元函数为类型Array增加输出到标准流。
class Array
{
int32_t *array_;
uint32_t size_;
friend std::ostream & operator << (std::ostream &os, Array const& obj);
//...
};
std::ostream & operator << (std::ostream &os, Array const& obj)
{
for(uint32_t i = 0; i < obj.size(); i++)
os << obj.array_[i] << " ";
os << std::endl;
return os;
}
3.2 友元类
友元类允许一个类访问另一个类的私有和保护成员,而不需要这两个类之间有继承关系。
友元类的主要作用:
- 作为某个类的数据操作辅助类,
- 作为多个类之间传递信息的辅助类。
需要注意的是,友元关系是单向的,不具有交换性,即A是B的友元,但B不一定是A的友元。此外,友元关系也不具有传递性,即B是A的友元,C是B的友元,但是C不一定是A的友元。
例如通过友元类ArrayHeler为Array增加打印和统计元素和功能。
class Array
{
int32_t *array_;
uint32_t size_;
friend class ArrayHeler;
//...
};
class ArrayHeler
{
public:
static void print(Array const& obj)
{
for(uint32_t i = 0; i < obj.size_; i++)
std::cout << obj.array_[i] << " ";
std::cout << std::endl;
}
static uint32_t sum(Array const& obj)
{
uint32_t s = 0;
for(uint32_t i = 0; i < obj.size_; i++)
s += obj.array_[i];
return s;
}
};
4 总结
C++使用类型机制对数据进行隐藏和封装,而友元机制则是破坏了类的封装性,但有时为了效率和系统设计该使用时就要大胆使用。