C++OO基础

Kiyotaka Wang Lv3

C++ OO基础

成员初始化表

  1. 构造函数的补充
  2. 执行
    • 先于构造函数体
    • 按类数据成员申明次序
class A
{
  int x;
  const int y;
  int &z;
  public:
  	A(): y(1), z(x), x(0) {x = 100;}
  // 先执行x = 0的赋值,因为我们先声明了数据成员x
}

//下面是一段问题代码,我们需要注意到给p初始化的时候size还没有初始化
class CString{
  char *p;
  int size;
  public:
  	CString(int x): size(x), p(new char[size]) {}
}

在C++98里面只有只有static const可以在类的内部初始化,其它类型都不可以像java一样直接在类的内部初始化。

使用初始化表可以减轻编译器Compiler的负担。

在C++11之后可以在类的内部初始化,在底层改进之后两种方式的效率已经差不多了,而且可以省去构造函数重载的时候重复初始化同一个变量的麻烦。

析构函数

  1. 格式:~<类名>()
  2. 功能:RAII:Resource Acquisition Is Initialization(资源获取即初始化)
  3. 调用情况
    1. 对象消亡时,系统自动调用
    2. C++离开作用域的时候回收
    3. 使用delete关键字的时候进行调用
  4. Private的析构函数:(强制自主控制对象存储分配)
    1. 回收对象的过程被接管,保证对象在堆上进行创建,但是不能使用delete,那么我们可以在内容提供一个destroy()方法来进行回收
    2. 写在栈或者全局区是不能通过编译的(自动调用,发现调不到)
    3. 强制在堆上进行创建,对很大的对象而言有好处——强制管理存储分配
    4. 适用于内存栈比较小的嵌入式系统

为什么不像java一样实现GC?

  1. 效率障碍

  2. 存在不能用GC的场合

RAII: Resource Acquisition Is Initialization

释放对象持有的非内存资源

class A{
    public:
        A();
        void destroy(){delete this;}
    private:
        ~A();
};
//析构函数私有,无法声明
A a;
int main(){
    A aa;//析构函数私有,无法声明
};
A *p = new A;//在堆上声明
delete p;//错误
p->destroy();//可能出现p的null空指针问题

//Better Solution
static void free(A *p){ delete p; }
A::free(p);

这边对象的数组不会因为类的消亡而被释放,因此我们需要自己实现一个析构函数。

拷贝构造函数

Copy Constructor

  1. 创建对象时,用一同类的对象对其初始化
  2. 自动调用
A a;
A b = a;
A f(){
	A a;
	...
	return a;
}

f();

f(A a){
...
}

A b;;
f(b);

因此我们要求拷贝的时候

public:
	A(const A& a);

需要传一个引用。

默认拷贝构造函数

  • 逐个成员初始化(member-wise initialization)
  • 对于对象成员,该定义是递归的

何时需要copy constructor?

防止出现悬挂指针的情况。

因此当我们需要深拷贝的时候需要自己实现拷贝构造函数。

//实例
class A { 
	int x, y;
	public:
		A() { x = y = 0; }
		void inc() { x++; y++; }
};
class B {
	int z;
	A a;//已经默认创建了
	public:
		B(){ z = 0; }
		B(const B& b):{ z = b.z; }
		void inc() { z++; a.inc(); }//拷贝构造函数
};
int main() {
	B b1;    //b1.z = b1.a.x = b1.a.y =0 
	b1.inc();//b1.a.x = b1.a.y = b1.z=1 
	B b2(b1);//b2.z=1 b2.a.x=0 b2.a.y=0,这个时候调用的是A的默认构造函数
}

拷贝构造函数的两条规则:

包含成员对象的类

  1. 默认拷贝构造函数:调用成员对象拷贝构造函数
  2. 自定义拷贝构造函数:调用成员对象的默认构造函数:程序员如果接管这件事情,则编译器不再负责任何默认参数。

可以参考这篇博客http://t.csdn.cn/Q6cie

移动构造函数

string generate(){
	...
	return string("test");
}
string S = generate();
//这个程序先创建了一个对象,再调用了一次拷贝构造函数,因此效率是很低的
//这里就需要我们使用移动构造函数了

右值引用A&&

先谈谈左值引用,可以把一个非常数的变量绑定到等式左边的一个变量上面,如上面的int &y = x, const int &z = x;

而对于上面的generate函数返回的右值,右值引用则可以支持绑定到右值上面,而不会绑定到左值上面。对于string或者vector这种ADT,拷贝构造函数的代价是很大的,因此我们就需要想办法避免用拷贝构造函数的方法构造。

而如图中的移动构造函数,我们在移动的时候最后将s.p置为空,避免了悬挂指针的问题。

所以像之前的string S = generate()就可以改为string &&S = generate()。这样更efficient,降低了拷贝的代价。

五三原则:拷贝构造、拷贝赋值、析构函数、移动构造、移动复制

  1. 在你需要对上面的五个函数中的一个进行自定义的时候,那么其他的四个编译器也不会进行默认方法的调用,都需要自己实现自定义。
  2. 在考试和机考中或许我们不会需要自己实现(也不做要求),但是作为一门高级程序语言,我们需要掌握这些技巧。

动态对象

  • 在heap中创建
  • new/delete

不用malloc和free,因为new可以调用对象的构造函数,而malloc是不行的。

为什么要引入new、delete操作符

调用constructor和deconstructor

此外,new是可以重载的,而malloc不可以。因为我们可以写带不同参数的构造函数。

栈上的对象都是有名对象,可以通过名称去访问,而堆上的都是无名对象,只能通过指针去访问。也就是说所有new出来的都是无名对象,只能通过指针去访问。

primitive是基本数据类型的意思

对象删除

  • delete
    • called on the pointer to an object
    • Works with primitives & class-types
  • Syntax
    • delete ptrName
  • Example
delete intPtr;
intPtr = NULL;

delete carPtr;
carPtr = NULL;

delete custPtr;
custPtr = NULL;
//我们需要将指针置为nullptr,以防止后面发生段错误,访问了不该访问的东西以致于自己都不知道哪边错了。

动态对象数组

  • 动态对象数组的创建与撤销
A *p;
p = new A[100];
delete []p;
  • 注意
    • 不能显式初始化,相应的类必须有默认构造函数(可以忽略掉,新版本的c++是可以的)
    • delete中的[]不能省(会少调用析构函数,并且会产生内存泄漏、段错误)

不过对于下面的情况是可以不需要[]的

int *p;
p = new int[100];
delete p;//对于内置的数据类型是不用调用析构函数的。但为了养成良好的习惯,我们要求new []和delete []是成对出现。

解释

对于自己定义的数据类型,我们需要在前面空出四个字节去存储数组的长度,用来调用析构函数。但是对于内置的数据类型,我们不需要调用析构函数,因此在int [100]时我们是不会留出四个字节的,这样子我们delete p的时候直接释放的就是100个int的空间。而自定义类型,因为没有长度,所以会少调用析构函数,实际上只delete了第一个对象。

现在的C++版本支持显示初始化

class A{
	public:
  	A(int i){...}
}

int main(){
  A *p = new A[5]{0,1,2,3,4,};//类比new int[5] {0,1,2,3,4}
}

动态2D数组

实际上多维数组更常用的方法是用一维数组模拟,操作时进行一波换算即可。

Const 成员

  • const成员变量
class A {
	const int x;
}
  • 初始化放在构造函数的成员初始化表中进行
class A {
  const int x;
  public:
  	A(int c): x(c) {}
}

static const的值必须在类中定义的地方就初始化,因为static是所有对象共享的,所以不能由某个对象创建的时候初始化,因此我们需要在定义的时候就初始化。

当我们声明了一个const的对象,编译器如何判断哪些函数能够调用?

  • const成员函数
    • 对于没有声明成const的函数,调用它是不允许的

如果有人尝试在f函数后面加一个const以欺骗compiler?

编译器会报错,因为函数本身就修改了成员变量,不能标识为const。

正常来讲编译结果是这样的。show的标红的const表示A*的内容不能改变,const this则表示这个对象不能改变。const是函数重载的参数。

下面这个例子的f是可以标识为const的,因为indirect_int是一个引用,指向了堆上面的内存,属于外部变量,引用本身是不能修改的,我们修改的是指向的内容。

class A{
    int a;
    int & indirect_int;
    public:
        A():indirect_int(*new int){ ... }
        ~A() {
            delete &indirect_int;
        }
        void f() const{
            //只要不是直接修改变量的值就OK
            //引用本身是不能修改的,所以编译器认为没问题
            indirect_int++;//只是指向的内容发生了变化
        }
};
//用a来做初始化
  1. 关键词mutable:表示成员可以再const中进行修改,而不是用间接的方式来做。
  2. 去掉const转换:(const_cast)<A*>(this)->x转换后可以修改原来的成员

静态成员

静态成员变量

  1. 类刻划了一组具有相同属性的对象
  2. 对象是类的实例
  3. 静态成员被类对象所共享,唯一拷贝,通过类访问控制

问题:同一个类的不同对象如何共享变量?

  1. 如果把这些共享变量定义为全局变量,则缺乏数据保护
  2. 名污染
  3. 为了保证只能被定义一次,因此不能写在.h中,要写在.cpp中(参考博客http://t.csdn.cn/3yJqm)

静态成员函数

  1. 可以在没有对象的情况下使用

  2. 只能存取静态成员变量,调用静态成员函数

  3. 遵循类访问控制

静态成员的使用

  1. 通过对象使用/通过类使用
  2. C++支持观点“类也是对象”

Resource control懒初始化原理

原则:谁创建,谁归还

将构造函数、拷贝构造函数、移动构造函数设为private,则只能在类内new

封装了对象的创建过程,使得由创建者负责整个初始化过程,而使用者只能调用static方法来调用。

友元

类外部不能访问该类的private成员

  1. 通过该类的public方法
  2. 会降低对private成员的访问效率,缺乏灵活性
  3. 例如:矩阵类(Matrix)、向量类(Vector)和全局函数(multiply),全局函数实现矩阵和向量相乘

分类

  1. 友元函数
  2. 友元类
  3. 友元类成员函数

作用

  1. 提高程序设计灵活性
  2. 数据保护和对数据的存取效率之间的一个折中方案

一些问题:

没有 classC,可以声明友元吗?不可以,因为需要知道内存空间【先声明后使用】

没有 classB,可以声明友元吗?可以

第一种情况:friend class B:

  • 编译器会寻找有没有类 B
  • 如果没有则会引入一个 B

第二种情况:friend B:

  • 省略关键字的时候不会引入 B,如果没有 B 会报错模板类

  • 但是这种形式常用于模板类 (T 或者 typedef 的时候来写)

一个错误的事例,因为Matrix调用Vector的时候还没有声明Vector,所以无法调用,但是声明可以是不完全的,所以我们可以先不完全声明一下Vector。

两个类相互引用

  1. 为了隔离数据,权限控制
  2. 为了同时运行,派生出不同的变化
class B;
class A{
  int a;
  public:
  	void show(B &b);
  	friend class B;
}

class B{
  int b;
  public:
  	void show(A &a);
  	friend class A; 
}
void A::show(B &b){
  std::cout << b.b;
}

需要尽量满足迪米特法则,努力让接口完满且最小化。

迪米特法则LoD

  • Each unit should have only limited knowledge about other units: only units "closely" related to the current unit.
  • Each unit should only talk to its friends; don't talk to strangers.
  • Only talk to your immediate friends.

迪米特法则 (Law of Demeter) 又叫做最少知识原则,也就是说,一个对象应当对其他对象尽可能少的了解。不和陌生人说话。英文简写为: LoD。

迪米特法则的目的在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。

迪米特法则不希望类之间建立直接的联系。如果真的有需要建立联系,也希望能通过它的友元类来转达。因此,应用迪米特法则有可能造成的一个后果就是:系统中存在大量的中介类,这些类之所以存在完全是为了传递类之间的相互调用关系 —— 这在一定程度上增加了系统的复杂度。

友元的具体写法可以看这篇博客(55条消息) C++:友元(看这一篇就够了)_孙 悟 空的博客-CSDN博客_友元

  • 标题: C++OO基础
  • 作者: Kiyotaka Wang
  • 创建于 : 2022-11-06 22:56:15
  • 更新于 : 2024-01-15 12:50:09
  • 链接: https://hmwang2002.github.io/2022/11/06/c-oo-ji-chu/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。