Jun's Blog

浅谈C++中的类

· Jun

类的基本思想

  • 数据抽象
  • 封装
    • 接口
    • 实现

类的性质

构造

当一个对象被创造出来,就称之为构造.构造的过程实际上是调用了类定义的构造函数

构造函数不能被声明为const

直接初始化构造

这种构造方式调用了初始化构造函数,用参数列表中的参数生成了一个对象

此构造函数可以被重载,有多个,实际调用参数列表最匹配的那个构造函数

1
std::string str("init")

这里是直接初始化,我们实际上是调用了string类的初始化函数,参数是"init",生成了一个对象

拷贝构造

拷贝初始化

利用一个已有的对象初始化另一个对象,称为拷贝初始化

拷贝构造如果需要的话还会进行类型转换,所以一般其不应该为explicit

1
Foo(const Foo&);

拷贝构造函数的型参应该为引用类型,因为为了调用拷贝构造函数,我们必须拷贝他的实参,然后我们又将调用拷贝构造函数,形成死循环

1
2
std::string s1("direct_init");
std::string s2 = s1;

这里是拷贝初始化,我们实际上调用了拷贝构造函数,来生成了一个对象

拷贝赋值运算符

一个已初始化的对象,进行赋值操作时,实际上调用的就是复制运算符

它实际上执行了拷贝构造函数和析构函数的工作

1
2
3
std::string s1("direct_init");
std::string s2;
s2 = s1;
阻止拷贝

我们可以在参数列表后加上=delete,表示我们禁止拷贝操作

例如iostream阻止了拷贝,避免多个对象写入或读取相同的IO缓冲

移动构造

Copy can be expensive. --Bjarne Stroustrup

在重新分配内存时,将旧内存拷贝进新元素是不划算的,尤其是旧元素在此之后将不再被使用时

一个更好的办法是移动元素,将资源直接转交给新对象

1
2
3
4
5
Foo::Foo(Foo &&f) noexcept : 
	a(f.a), b(f.b) //直接移动资源
	{
	f.a = f.b = nullptr;//源对象之后必须指向不再移动的资源,要保持它进入可析构的状态
	}
移动赋值运算符

移动赋值运算符实际上执行了移动构造函数和析构函数的工作

右值引用

指必须绑定到右值的引用

它只能绑定到即将被销毁的对象上

右值一般是字面量,或求值过程中临时创建的对象

我们可以显式地将一个左值转换为右值std::move(),于是便可以将右值引用绑定在上面

析构

当一个对象被销毁时,就会自动调用它自身定义的析构函数

总结

以上为类的基本属性,每当使用了一个属性本质上就是调用了相关的函数或方法

编译器的合成行为

如果我们需要编译器生成默认的行为,我们可以在参数列表后架上=default显式地要求生成合成的方法

合成的拷贝构造函数

如果我们没有定义拷贝构造函数,那么编译器会自动生成一个

它实际上是将对象的成员逐一拷贝给另一个对象

  • 如果成员为另一个类,就调用他本身的拷贝构造函数
  • 如果类的成员为内置类型,就直接拷贝
  • 如果成员为数组,就逐元素拷贝成员

合成的移动操作

当我们没有定义拷贝构造函数时,且成员均是可移动时,编译器将自动生成

当我们定义了拷贝构造函数,且无自定义移动行为时,编译器将会把移动操作定义为删除的函数

当我们未拷贝构造函数,且成员不可移动,编译器将会把移动操作定义为删除的函数

访问控制

public

在整个程序中都能被访问,为接口

private

只可以被类的成员函数访问

protexted

自身和其派生类有权访问

友元

类可以允许其他类或函数访问他的非公有成员,声明了友元即可

类的静态成员

声明

1
2
3
4
5
6
class Foo {
	public:
		static int a;
	private:
		static double b;
}

初始化

通常情况下,我们不应该在类的内部初始化类的静态成员

不过我们可以为静态成员提供const整数类型的类内初始值,且要求静态成员必须为constepxr

1
2
3
class Bar {
	static constexpr int a = 10;
}

性质

静态类的成员不与任何对象绑定在一起,也不能被声明为const

访问

1
int c = Foo::a;