C++面向对象基础(二)

目录

一、类

C++ 中可以使用 struct、class 来定义一个类。struct 的默认成员权限是 public,class 的默认成员权限是 private。除此之外,二者基本无差别。struct 中同样可以存在成员函数。

struct Person {
    //private:
    // 成员变量
    int age;
    // 成员函数
    void run() {
        cout << " age is " <<this -> age << endl;
    }
}

不同于OC,C++ 中的对象既可以存在栈区,也可以存在堆区。存在于栈空间的对象称为自动 (auto) 变量。

    // 在栈空间分配内存给person对象
    // 这个person对象的内存会自动回收,不用开发人员去管理
    Person person;//调用此行代码,此时已经初始化完毕
    person.age = 20;
    person.run();

二、对象的内存布局和 this

    Person person;
    person.age = 20;
    person.run();
    cout << sizeof(person) << endl;

上述代码打印结果为 4 ,4 仅表示成员变量所占据的内存空间。成员函数不存在对象内存空间中,原因在于成员函数是不同对象公共使用的,如果每个对象的内存空间都拷贝一份会造成不必要的浪费。实际情况,对象在调用成员函数的时候,会自动传入当前对象的内存地址,也即 thisthis 是指向当前对象的指针。注意不能使用this.age 访问成员变量,this 是指针,必须使用 this -> age 形式。这点可以结合汇编知识猜测到,因为混编代码中函数的调用形式为call + 函数地址

// 成员函数
void run() {
    //编译器允许省略 this ->
     cout << "age is " << this -> age << endl;
}

注意:C++ 中不存在方法缓存一说,OC 中之所以存在方法缓存主要是因为有类对象的存在。

三、堆空间(申请、释放、清零)

堆空间的申请和释放可以通过以下两种形式:

  • malloc | free
  • new | delete
int size = sizeof(int);
// 申请4个字节的堆空间内存
int *p = (int*)malloc(size);
// 从p开始的4个字节, 每个字节都存放0
memset(p, 0, size);
//将10放到内存空间中
*p = 10;

//申请4个字节空间,内部存放 char 类型
*char *p = (char *)malloc(4);
*p = 1;
*(p + 1) = 2;
*(p + 2) = 3;
*(p + 3) = 4;
// 没有初始化
int *p1 = new int;
// 初始化为0
int *p2 = new int();
// 初始化为5
int *p3 = new int(5);
// 没有初始化
int *p4 = new int[3];
// 全部元素初始化为0
int *p5 = new int[3]();
// 全部元素初始化为0
int *p6 = new int[3]{};
// 首元素初始化为5,其他元素初始化为0
int *p7 = new int[3]{ 5 };delete p1;
struct Person {
    int m_age;
};
// 堆空间
Person *p = new Person();
p->m_age = 20;
delete p;

四、对象的内存位置(全局、栈、堆)

C++ 中的对象可以存在全局区、栈空间或堆空间。

struct Person {
    int m_age;
};
// 全局区
Person g_person;
int main() {
    // 栈空间
    Person person;
    // 堆空间,相比于malloc、free,更推荐此种方式
    Person *p = new Person();
    p->m_age = 20;
    delete p;

         // 堆空间
         Person *p2 = (Person *) malloc(sizeof(Person));
    free(p2);
    return 0;
}

五、构造函数

构造函数,也叫构造器,在对象创建的时候自动调用。主要有以下特点:

  • 函数名与类同名,无返回值(void都不能写),可以有参数,可以重载,可以有多个构造函数。
  • 注意一旦自定义了构造函数,必须用其中一个自定义的构造函数来初始化对象。
  • 通过 malloc 创建的对象不会调用构造函数,所以一般建议使用 new 创建对象。
  • 要声明为 public ,才能被外界正常使用。
struct Person {
    int m_age;
    Person() {
        cout << "Person()" << endl;
        // this->m_age = 0;
        memset(this, 0, sizeof(Person));
    }
    Person(int age) {
        cout << "Person(int age)" << endl;
        this->m_age = age;
    }
};
int main() {
    // 栈空间
    Person person1; //调用Person()
    Person person2(); //不会调用构造函数,这个只是函数声明,函数名叫person2,无参,返回值类型是Person
    Person person3(20);//调用Person(int age)
    // 堆空间
    Person *p1 = new Person; //调用Person()
    Person *p2 = new Person(); //调用Person()
    Person *p3 = new Person(30); //调用Person(int age)
    return 0;
}

六、成员变量初始化方式

变量的初始化有两种形式,一种是在构造函数实现,另一种是在没有构造函数的情况下 new 创建对象的时候添加小括号。

方式一:

struct Person {
    int m_age;
    Person() {
        cout << "Person()" << endl;
        //以下两种形式都可以初始化成员变量,更推荐第二种。
        // this->m_age = 0;
        memset(this, 0, sizeof(Person));
    }
};

方式二:

// 全局区(成员变量初始化为0)
Person g_person;
int main() {
    // 栈空间(成员变量没有初始化)
    Person person;
    // 堆空间
    Person *p1 = new Person; // 成员变量没有初始化
    Person *p2 = new Person(); // 成员变量有初始化
    return 0;
}

七、析构函数(Destructor)

析构函数,也叫析构器,在对象销毁的时候自动调用。有以下特点:

  • 函数名以~开头,与类同名,无返回值(void都不能写),无参,不可以重载,有且只有一个析构函数。
  • 通过malloc分配的对象free的时候不会调用构造函数。
  • 和构造函数一样,要声明为 public ,才能被外界正常使用。
class Person {
    int m_age;
public:
    // 对象创建完毕的时候调用
    Person() {
        cout << "Person()" << endl;
        this->m_age = 0;
    }
    Person(int age) {
        cout << "Person(int age)" << endl;
        this->m_age = age;
    }
    // 对象销毁(内存被回收)的时候调用析构函数
    ~Person() {
        cout << "~Person()" << endl;
    }
};

八、对象内部内存管理(对象的成员变量为对象)

当对象内部的成员变量为对象时,对象销毁的时候,成员变量对象也会被销毁。但当对象内部的成员变量为对象指针时,可以在对象内部申请堆空间,并由对象内部回收堆空间。

struct Car {
    int m_price;
    Car() {
        cout << "Car()" << endl;
    }
    ~Car() {
        cout << "~Car()" << endl;
    }
};

成员变量为对象时,构造函数和析构函数都无需做额外处理:

struct Person {
    int m_age; 
    Car m_car; 
    Person() {
        cout << "Person()" << endl;
    }
    ~Person() {
        cout << "~Person()" << endl;
    }
};

成员变量为对象指针时,构造函数和析构函数都要做处理:

struct Person {
    int m_age; //4
    Car *m_car; //4
    Person() {
        cout << "Person()" << endl;
         this->m_car = new Car();
                 //注意不能这样写,因为此时Car对象在栈区,出了构造函数栈区被回收,此时指针指向栈区很危险。
        /*Car car;
        this->m_car = &car;*/
    }
    // 内存回收、清理工作(回收Person对象内部申请的堆空间)
    ~Person() {
        cout << "~Person()" << endl;
        delete this->m_car;
    }
};

使用:

Person *p = new Person();
delete p;

九、方法声明和实现分离

Person.h 文件中的声明:

class Person {
    int m_age;
public:
    Person();
    ~Person();
    void setAge(int age);
    int getAge();
};

Person.cpp 文件中的实现:

Person::Person() {
    cout << "Person()" << endl;
}
Person::~Person() {
    cout << "~Person()" << endl;
}
void Person::setAge(int age) {
    this->m_age = age;
}
int Person::getAge() {
    return this->m_age;
}

十、命名空间

命名空间主要用于避免命名冲突。Java 中的命名空间是所谓的 package,OC 中没有命名空间,一般可以用类前缀作区分。

namespace ZW {
    class Car {
    public:
        Car();
        ~Car();
    };
}
int main() {
    ZW::Car car;
    return 0;
}

命名空间可以嵌套。

namespace AA {
    namespace BB {
        int g_no;
        class Person {

        };
        void test() {

        }
    }
}

int main() {
    using namespace AA;
    BB::g_no = 30;

    using namespace AA::BB;
    g_no = 10;
    return 0;
}

十一、继承以及内存布局

OC 中所有的对象都继承自 NSObject,Java 中所有的对象都继承自 java.lang.object 。但 C++ 不同于 OC 和 Java,C++ 没有最终的基类。

struct Person {
    int m_age;
};
struct Student : Person {
    int m_no;
};
struct GoodStudent : Student {
    int m_money;
};
int main(int argc, const char * argv[]) {
    Person person;// 4
    Student stu;// 8
    GoodStudent gs;// 12
    gs.m_age = 20;
    gs.m_no = 1;
    gs.m_money = 666;
    cout << sizeof(Person) << endl;//4
    cout << sizeof(Student) << endl;//8
    cout << sizeof(GoodStudent) << endl;//12
    return 0;
}

下图是 GoodStudent 的内存布局。其中,父类的成员变量在前,子类的成员变量在后。


十二、初始化列表和默认参数

初始化列表

初始化列表一种便捷的初始化成员变量的方式,只能用在构造函数中,初始化顺序只跟成员变量的声明顺序有关。下面代码段中注释的代码可以用一种更简洁的方式表达。

struct Person {
    int m_age;
    int m_height;
    Person(int age, int height) :m_age(age), m_height(height) {
        
    }
    /*Person(int age, int height) {
        cout << "Person(int age, int height) " << this << endl;
        this->m_age = age;
        this->m_height = height;
    }*/
默认参数

默认参数只能写在函数的声明中,构造函数的初始化列表只能写在实现中。

//类声明
class Person {
    int m_age;
    int m_height;
public:
    // 默认参数只能写在函数的声明中
    Person(int age = 0, int height = 0);
};
//实现
// 构造函数的初始化列表只能写在实现中
Person::Person(int age, int height) :m_age(age), m_height(height) {

}
int main() {
    Person person;
    Person person2(10);
    Person person3(20, 180);
    return 0;
}

十三、父类的构造函数

  • 1、子类的构造函数默认会调用父类的无参构造函数。父类的构造函数先于子类的构造函数调用。
  • 2、如果子类的构造函数显式地调用了父类的有参构造函数,就不会再去默认调用父类的无参构造函数。
  • 3、如果父类没有无参构造函数,子类的构造函数必须显式调用父类的有参构造函数。
class Person {
    int m_age;
public:
    Person(int age) :m_age(age) {
        cout << "Person(int age)" << endl;
    }
};
class Student : public Person {
    int m_score;
public:
    Student() :Person(0) {

    }
};

十四、虚函数

虚函数是被 virtual 修饰的成员函数,C++中的多态通过虚函数(virtual function)来实现,只要在父类中声明为虚函数,子类中重写的函数也自动变为虚函数。C++ 中的多态不像 OC 那样智能,如果不使用 virtual 关键字,方法调用者只会根据指针类型去寻找方法并调用,而不是根据对象类型去寻找方法。所以 C++ 若想实现多态,一般要借助 virtual 关键字。

class Animal {
public:
    int m_age;
    virtual void speak() {
        cout << "Animal::speak()" << endl;
    }
    virtual void run() {
        cout << "Animal::run()" << endl;
    }
};
class Cat : public Animal {
public:
    int m_life;
    Cat() :m_life(0) {}
    void speak() {
        cout << "Cat::speak()" << endl;
    }
    void run() {
        cout << "Cat::run()" << endl;
    }
};
Animal *cat1 = new Cat();
cat1->speak();
cat1->run();

十六、虚表

虚函数的实现原理是虚表(也称为虚函数表),虚表里面存储着最终需要调用的虚函数地址。

在没有虚函数的情况下,对象的内存空间为成员变量占据的空间大小。virtual 关键字修饰之后,对象内存会增加 4 个字节,主要用于存储虚表地址,虚表内存中存储着虚函数地址(如果子类没有重写父类虚函数,虚表中存储父类的虚函数地址)。如果对象内存中存在虚表地址,会优先调用根据虚表中的函数地址调用对应的函数,以保证调用对应类型对象的方法;如果不存在虚表,就只能依据编译器特性根据指针类型对调用对应的方法。注意:每创建一个 Cat 对象内部都包含虚表地址,但是这些 Cat 对象的虚表地址指向同一张虚表内存空间。

虚表内存布局

十七、纯虚函数

纯虚函数是指没有函数实现且初始化为 0 的虚函数,主要用来定义接口规范。纯虚函数对应的类也被称为抽象类,抽象类不可以实例化,只有被继承使用才有意义。抽象类也可以包含非纯虚函数,如果父类是抽象类,子类没有完全实现纯虚函数,那么这个子类依然是抽象类。

//这是一个纯虚函数对应的类,它是一个抽象类,不能直接拿来初始化。
class Animal {
public:
    virtual void speak() = 0;//纯虚函数
    virtual void run() = 0;//纯虚函数
        void test{

        }
};
class Cat : public Animal {
public:
    void run() {
    }
};

十八、多继承

C++ 允许一个类可以有多个父类,但一般不建议使用,因为会增加程序设计复杂度。

class Student {
public:
    int m_score;
    void study() {
        cout << "Student::study() - score = " << m_score << endl;
    }
    void eat() {
        cout << "Student::eat()" << endl;
    }
};
class Worker {
public:
    int m_salary;
    void work() {
        cout << "Worker::work() - salary = " << m_salary << endl;
    }
    void eat() {
        cout << "Worker::eat()" << endl;
    }
};
class Undergraduate : public Student, public Worker {
public:
    int m_grade;
    void play() {
        cout << "Undergraduate::play() - grade = " << m_grade << endl;
    }
    void eat() {
        cout << "Undergraduate::eat()" << endl;
    }
};
多继承同名函数问题

多继承的同名函数主要通过命名空间来做区分。

    Undergraduate ug;
    ug.eat();
    ug.Worker::eat();
    ug.Student::eat();

十九、菱形继承和虚继承

菱形继承
class Person {
public:
    int m_age;
};
class Student : public Person {
public:
    int m_score;
};
class Worker : public Person {
public:
    int m_salary;
};
class Undergraduate : public Student, public Worker {
public:
    int m_grade;
};

菱形继承中最底部子类从基类继承的成员变量冗余、重复,所以最底部子类无法访问基类的成员,有二义性。即Undergraduate 对象无法访问基类 Person 的成员变量 m_age

虚继承

虚继承可以用来解决菱形继承问题。

class Student : virtual public Person {
public:
    int m_score;
};
class Worker : virtual public Person {
public:
    int m_salary;
};

二十、static

静态成员变量

static 修饰的成员变量称为静态成员变量,可以通过对象(对象.静态成员)、对象指针(对象指针->静态成员)、类访问(类名::静态成员)。存储在全局区,类似于全局变量,整个程序运行过程中只有一份内存。同全局变量最大的区别在于,静态成员变量限制了作用域,仅相关类可以直接使用。使用时需注意必须初始化,且必须在类外面初始化。

class Person {
public:
    static int ms_count;
};

//必须在类外面初始化
int Person::ms_count = 0;

int main() {
    Person::ms_count = 20;
    return 0;
}
静态成员函数

static 修饰的成员函数称为静态成员函数,存在于代码区。内部不能使用 this 指针,this 指针只能用在非静态成员函数内部。内部不能访问非静态成员变量\函数,只能访问静态成员变量\函数。不能是虚函数,虚函数只能是非静态成员函数。

class Car {
public:
    static int m_price;
    static void run() {
        cout << "run()" << endl;

    }
};
静态成员应用(单例)
class Rocket {
public:
    // C++:静态成员函数
    // Java、OC:类方法
        //提供一个公共的返回单例对象的静态成员函数
    static Rocket * sharedRocket() {
        // API p_thread
        if (ms_rocket == NULL) {
            ms_rocket = new Rocket();
        }
        return ms_rocket;
    }
    static void deleteRocket() {
        static Rocket *ms_rocket;
        if (ms_rocket == NULL) return;
        delete ms_rocket;
        ms_rocket = NULL;
    }
private:
       //使用 static 保证只有一个,私有化禁止外部访问
        static Rocket *ms_rocket;
        //禁止外部创建
    Rocket() {
        cout << "Rocket()" << endl;
    }
    ~Rocket() {
        cout << "~Rocket()" << endl;
    }
};

Rocket *Rocket::ms_rocket = NULL;

int main() {
    Rocket *p1 = Rocket::sharedRocket();
    // ......
    Rocket::deleteRocket();
    return 0;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345