分类 编程语言 下的文章

一、进程和线程

进程是表示资源分配的基本单位,又是调度运行的基本单位。例如,用户运行自己的程序,系统就创建一个进程,并为它分配资源,包括各种表格、内存空间、磁盘空间、I/O设备等。然后,把该进程放人进程的就绪队列。进程调度程序选中它,为它分配CPU以及其它有关资源,该进程才真正运行。所以,进程是系统中的并发执行的单位。

引入线程的目的是简化线程间的通信,以小的开销来提高进程的并发程度。有时称轻量级进程。 进程中的一个运行实体,是一个CPU调度单位, 资源的拥有者还是进程或称任务。事实上,引入线程主要是为了提高系统的执行效率,减少处理机的空转时间和调度切换(保护现场信息)的时间,以及便于系统管理。

- 阅读剩余部分 -

C++多态和虚函数的实现原理

一、关于多态

多态是C++中的重要内容,多态的意思是可以使用父类指针指向子类,并通过这个指针调用子类的函数。

多态可以分为两种形式:

  1. 编译时多态,主要指泛型编程。
  2. 运行时多态,主要指虚函数和子类继承。

一般我们讨论的都是运行时多态,运行时多态的几个基本条件为:

  • 继承和虚函数。
  • 父类对象指向子类对象。

多态形成的原理就是vptr指针和vtable虚函数表,当类中有虚函数时,编译器会自动生成虚函数表vtable,这个表属于这个类,所有类实例共享。同时还成一个vptr指针指向这个虚函数表,执行调用的时候,先通过vptr指针找到自己的虚函数表,然后执行表中的函数。

二、证明vptr指针存在

多态形成的必要条件是虚函数和继承,当两者其中之一都不存在的时候,编译器不会形成多态,因此也不会生成vptr指针。只有两者同时存在的时候,编译器才会生成vptr指针。

以下代码通过类的大小来证明了vptr指针的存在:

#include <iostream>
using namespace std;

class A {
public:
    A() {};
    ~A() {};
    void print() { cout << "AAAAAAAAAA" << endl; };
};

class B : public A {
public:
    B() {};
    ~B() {};
    void print() { cout << "BBBBBBBBBB" << endl; };
};

int main() {
    cout << "sizeof(A): " << sizeof(A) << endl;
    cout << "sizeof(B): " << sizeof(B) << endl;
    return 0;
}

C++对空类会分配一个字节的内存,此时的输出A和B的大小都是1:

sizeof(A): 1
sizeof(B): 1

将A中的print()函数设置为虚函数后:

virtual void print() { cout << "AAAAAAAAAA" << endl; };

两个结构体的大小会变成8,因为添加虚函数后,类中会生成vptr指针,64位系统指针是四个字节,所以A和B的大小就变成了8。

三、vptr指针的工作原理

3.1 vptr和vtable

在创建一个含有虚函数的类时,系统会为每个类生成虚函数表,存放了当前对象所有的函数列表,而vptr指针就指向这个表的地址。子类继承父类后,也会生成一个属于自己的虚函数表和vptr指针,如果子类有重写父类的虚函数,虚函数表中的函数地址就是自己类中的成员函数。执行子类调用时,先通过vptr指针定位到对应的虚函数表,然后执行对应的函数。

以上是一个图形示例,类B继承了类A,类B重写了f1f2函数,它的函数表中f1f2都指向自己的成员函数。而f3并未继承,所以指向父类。因此执行子类函数时,f1f2都是用的自己的成员函数。

关于静态联编和动态联编

说到多态肯定免不了要提到动态联编,动态联编就是指程序在运行时才能决定的运行片段,例如ifswitch代码段,它们只有在运行时才能知道下一步是什么,走哪个代码代码段。多态也是如此,运行到了虚函数的时候才能决定执行哪个函数。而静态联编就是指程序在编译阶段就能决定的事情,就像main函数,编译后就固定从它开始执行。

3.2 vptr指针绑定的顺序

子类中vptr指针的值的绑定顺序:

  1. 运行父类构造函数时,先指向父类的虚函数表。
  2. 运行子类构造函数时,再把指针指向子类的虚函数表。

通过以下代码和GDB调试器可以验证这个观点:

#include<iostream>

using namespace std;

class A {
public:
    A() {
    };
    ~A() {};
    virtual void print() { cout << "AAAAAAAAAA" << endl; };
};

class B : public A {
public:
    B() {
    };
    ~B() {};
    void print() { cout << "BBBBBBBBBB" << endl; };
};

int main() {
    A a;
    B b;

    return 0;
}

带调试模式编译:

g++ main.cpp -g -o app -std=c++11

使用GDB调试执行,先在父类构造函数中添加断点:

子类构造函数中也添加断点:

输入run执行,执行到第一个断点,是正在构造a对象的时候,此时打印出A的vptr指针地址:

继续往下执行,下一个断点依旧是在类A的构造函数处,此时正在构造对象b,因为B继承了A,所以先执行A的构造函数。此时打印出b的vptr指针地址,可以看到b对象的vptr指针和a对象中的vptr指针地址一样:

再继续执行,走到类B的构造函数,此时对象b的vptr指针就被修改成了类B自己的虚函数表地址了:

过程图示:

第一步,执行父类构造函数时,指向父类的虚函数表。

第二步,执行子类自己的构造函数时,再指向子类的虚函数表。

select模型是socket中的一种多路IO复用模型之一,通过轮询的方式来完成多路访问控制。

一个很简单的例子来描述select模型:

幼儿园老师要照顾所有的小朋友,每天他都会轮流去问小朋友:“小朋友小朋友,你饿了吗?”

如果小朋友饿了,那么老师就给这个小朋友喂饭,否则就开始询问下一个朋友,一直循环下去直到放学。

同时,如果班级里有其他的同学来了,也把他加到询问队列。如果有哪个同学生病了,则把它踢出询问队列。

select模型的原理就是这样,把所有连接的客户端socket加入到一个集合中去,然后一直不断轮询,判断哪一个socket有数据到达,就读取数据。否则继续轮询下一个数据。

linux系统在编译的时候就固定了select模型文件描述符集合的大小为1024个,这个大小无法更改,因此,select模型只适用于并发量小于1024个的服务连接。

> grep "FD_SETSIZE" /usr/include/ -R
/usr/include/linux/posix_types.h:#define __FD_SETSIZE    1024

- 阅读剩余部分 -

面向对象的三大基本特征:封装、继承和多态。类对象通过public/private/protected关键字实现对象的封装,封装后通过继承实现多样性,而这个多样性又需要通过多态来完成。

假设要实现一个攻击的功能,不同的角色战斗力的都不同,在以往的c中,要完成这个功能需要对每个不同的角色都添加一个攻击函数:

void attack_normal(obj n) { cout << "我砍了你一刀,你流了一滴血!" << endl;}
void attack_vip(obj v) { cout << "我是VIP,我的刀是屠龙宝刀,你流了十滴血!" << endl;} 
void attack_rmb(obj r) { cout << "我是RMB玩家,你已经死了!" << endl; }

而在有多多态后,所有的函数都可以合并为一个:

void attack(obj *o) {
    // 根据对象o的实际类型,攻击敌方。
}

- 阅读剩余部分 -

一、几者的区别

  1. malloc/free是c语言中分配内存空间的函数,malloc创建空间,free释放空间。
  2. new/delete是c++中分配内存的操作符,new创建空间,delete删除空间。
  3. new[]/delete[]也是C++中的操作符,用来给数组分配和释放空间。
  4. malloc只是简单的分配内存空间,而new分配空间后会自动调用对象的析构函数。相对应的,free也只是简单的删除内存空间,delete则会调用对应析构函数。

二、案例

定义一个简单的类:

class A{
public:
    A(){
        cout << "A()" << endl;
    }
    ~A(){
        cout << "~A()" << endl;
    }
}

使用malloc/freenew/delete分别创建和释放对象:

#include<iostream>
#include<stdlib.h>
using namespace std;
 
int main{
    cout << "---malloc/free---" << endl;
    A *a1 = (A*)malloc(sizeof(A)); 
    free(a1);
    cout << "---new/delete---" << endl;
    A *a2 = new A;
    delete a2;
    return 0;
}

运行:

---malloc/free---
---new/delete---
A()
~A()

可见,malloc确实没有调用构造函数,free也没有调用析构函数。

三、delete和delete[]

deletenew对应,delete[]new[]对应。delete用来删除单个对象,delete[]删除对象数组。

deletedelete[]的区别在于后者会调用数组内每一个元素的析构函数,而delete只会调用一个。两者在对于内置元素类型时功能一致,对于复杂类型delete可能会报错。

int main(){
    cout << "------" << endl;
    int *p1 = new int[10];
    delete p1;

    cout << "---delete[]---" << endl;
    A *a2 = new A[1];
    delete []a2;

    cout << "---delete---" << endl;
    A *a1 = new A[1];
    delete a1;
    return 0;
}

执行会报错:

可见,对内置类型而言,互相使用并没有问题。但是对自定义类型而言,delete和delete[]并不能乱用。