C++面向对象-类

C++中可以使用struct和class来定义一个类,在C++中,struct和class的区别是struct默认成员权限是public,而class成员权限是private,其它没有区别,为了体现封装性,实际开发中采用class声明一个类,看一个比较完整的C++类的声明:

#include <iostream>
using namespace std;

struct Person {
    int m_id;
    int m_age;
    int m_height;
    
    void display() {
        cout << "m_id = " << this->m_id << endl;
        cout << "m_age = " << this->m_age << endl;
        cout << "m_height = " << this->m_height << endl;
    }
};

int main() {
    Person person;
    person.m_id = 10;
    person.m_age = 20;
    person.m_height = 30;
    return 0;
}

这里声明的是一个栈对象,然后给对象赋值成员变量,这里需要说下this指针,在调用display成员函数的时候,编译器自动会将调用这个函数的对象地址传进去,编译器会帮我们生成this指针保存这个对象地址,有点像是多传了一个参数到成员函数便于访问。这里需要强调的是:类的内存是不包括函数的,函数地址是编译后就固定的,跟对象内存不在一个地方。sizeof(person)得出的大小是12,每个int占四个字节,正由于如此,函数代码能通过this找到对象的成员。结论:this指针指向调用该函数的对象,观察以下代码是否正常输出:

struct Person {
    int m_id;
    int m_age;
    int m_height;
    void display() {
        cout << "display" << endl;
    }
};

int main() {
    Person *person = nullptr;
    person->display();
    return 0;
}

答案是:正常打印函数,如果函数没有调用this访问成员变量程序是不会崩溃的。

堆空间

每个应用都有自己独立的内存空间,其内存空间一般都有以下几大区域

  • 代码段(代码区):用于存放代码
  • 数据段(全局区):用于存放全局变量等
  • 栈空间:每调用一个函数就会给它分配一段连续的栈空间,等函数调用完毕后会自动回收这段栈空间,自动分配和回收
  • 堆空间:需要主动去申请和释放
    以上代码声明对象是通过是在栈上声明的,如果是堆对象初始化,代码如下:
Person *per = new Person();

这样申请的内存必须通过析构函数回收堆内存,否则造成内存泄露。

 delete per;

堆空间初始化,下面用int类型的指针举例,其它数据类型类似:

int *p1 = (int *)malloc(sizeof(int)); // *p1未初始化
int *p2 = (int *)malloc(sizeof(int));
memset(p2, 0, sizeof(int)); //*p2的每一个字节初始化为0
int *p3 = new int; //*p3未被初始化
int *p4 = new int();//*p4被初始化为0
int *p5 = new int(5);//*p5被初始化为5
int *p6 = new int[3]; //3个元素的数组元素未被初始化
int *p7 = new int[3](); //3个元素的数组被初始化为0
int *p8 = new int[3]{}; //3个元素的数组被初始化为0
int *p9 = new int[3]{5}; //3个元素的首元素被初始化5,其它元素为0
int *p10 = new int[3]{5,4,3}; //3个元素的首元素分别被初始化5、4、3

对象

对象的内存一般分布在三个3个区域:

  • 全局变量:全局变量存在于数据段
  • 堆空间:动态申请的内存(malloc,new等)
  • 栈空间:函数里面的局部变量
Person g_person; //全局对象
int main(int argc, const char * argv[]) {
    Person *p = new Person();//堆空间
    Person person; //栈空间
}

一般对象的初始化后会调用构造函数,用于对象的初始化,构造函数有以下特点:

  • 函数名与类名相同,无返回值(void)都不能写,可以有参数,可以重载,可以有多个构造函数
  • 一旦自定义了构造函数,必须用其中一个自定义构造函数去初始化
  • malloc分配内存的时候不会调用构造函数,C++中一般用new
  • 如果类中没有构造函数,在某些特定情况下编译器才会为类生成一个默认的无参构造函数。
    具体请看下面的构造函数注意有参构造和无参构造的函数调用次数,声明对象需要跟函数声明区分开,成员变量用memset比较快进行初始化:
#include <iostream>
using namespace std;

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;
    }
};

// 全局区
Person g_person1; // Person()
Person g_person2(); // 这是一个函数声明,函数名叫g_person2,无参,返回值类型是Person
Person g_person3(10); // Person(int 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)

    // 4次无参构造函数
    // 3次有参构造函数

    getchar();
    return 0;
}

C++中有构造函数产生对象,也有析构函数,析构函数负责回收堆空间内存,注意点如下:

  • 析构函数(也叫析构器),在对象销毁的时候自动调用,一般用于完成对象的清理工作
  • 通过malloc分配的对象free的时候不会调用析构函数
  • 构造函数、析构函数要声明为public,才能被外界正常使用
  • 析构函数一般一个new对应一个delete,在对象中又声明对象的时候要记得释放对象。
#include <iostream>
using namespace std;

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

struct Person {
    int m_age; // 4
    Car *m_car; // 4
    // 初始化工作
    Person() {
        cout << "Person()" << endl;
         this->m_car = new Car();
    }

    // 内存回收、清理工作(回收Person对象内部申请的堆空间)
    ~Person() {
        cout << "~Person()" << endl;

        delete this->m_car;
    }
};

void test() {
    Person *p = new Person();
    delete p;
}

int main() {
    test();
    return 0;
}

在实际开发中,一个类的声明和实现一般是分开的,比如,在person.h中声明:

#pragma once

// 声明 .h 头文件
class Person {
    int m_age;
public:
    Person();
    ~Person();
    void setAge(int age);
    int getAge();
};

person.cpp中实现如下:

#include "Person.h"
#include <iostream>
using namespace std;

// ::是域运算符
// 实现 .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;
}

在函数名前写上类名然后跟一个::才能实现类的里面一些成员函数。在上面一些代码中我们经常看到using name space std类似的声明,这里就牵扯出一个C++经常使用的概念命名空间,可以用来避免命名冲突。命名空间注意事项如下:

  • 命名空间不影响内存布局
  • 命名空间里面可以嵌套命名空间
  • 如果用using声明多个命名空间,外界访问相同的变量会生成二义性而报
  • 相同的命名空间可以合并,命名空间最外面是有一个空的命名空间,只是一般省略
namespace MyNameSpace {
    int g_age;  //全局变量
    class Person{
        int  g_age;  //类的成员变量
        void test(){} //类的成员函数
    }; //声明一个类
    void test(){} //类外面的函数
};
int main(int argc, const char * argv[]) {
    ::MyNameSpace::Person p;
    MyNameSpace::test();
}

继承

继承,可以让子类拥有父类的所有成员(变量\函数)

struct Person {
    int m_age;
    void run() {
        cout << "run()" << endl;
    }
};

struct Student : Person {
    int m_score;
    void study() {
        cout << "study()" << endl;
    }
};
int main {
    Student student;
    student.m_age = 18;
    student.m_score = 100;
    student.run();
    student.study();
    return 0;
}

注意:如果继承父类子类含有同名变量,这个不会覆盖,而是会同时继承下来,所以使用的时候需要制定域名作用符区分,也就是必须制定是父类还是子类。

#include <iostream>

using namespace std;

struct Person
{
    int m_age;
    void run(){
        cout << "person run()" << endl;
    }
};

struct Student:Person
{
    int m_age;
    void run(){
        cout << "Student run()" << endl;
    }
};

int main(int argc, const char * argv[]) {
    
    Student stu;
    stu.Person::m_age = 100;  //同名变量必须域名作用符区分赋值变量
    stu.Student::m_age = 100;
    stu.Person::run();  //同名成员函数必须域名作用符区分赋值变量
    stu.run(); //不制定域名作符号,默认是使用子类
    return 0;
}

关于对象的内存布局,先是父类成员变量再是子类成员变量,成员函数在代码段,不占用对象内存,仅仅只是把对象地址传给了函数,提供一个类似的接口便于成员函数访问。

访问权限

C++中继承方式一般有3种:
1.public:公共的,任务地方都可以访问,struct默认是public,继承也是public继承
2.protected:保护的,自己和子类可以访问
3.private:私有,只有自己类内部可以访问
关于子类访问父类成员变量是否有权限,取以下2个访问权限最小权限:
1.成员本身的权限
2.上一级父类的继承方式,如果没有继承就忽略
下面举一个关于继承的列子:

class Person {
public:
    int m_age;
    void run() {
        
    }
};

class Student : public Person {
public:
    int m_no;
    void study() {
        m_age = 10;  //可以访问,因为该成员变量是
    }
};

class GoodStudent : private Student {
public:
    int m_money;
    void work() {
        m_age = 10;  //m_age 本身是public,父类继承是保护继承,可访问
        m_no = 100;  //m_no 本身是public,父类继承是保护继承,可访问  
    }
};


class veryGoodStudent : public GoodStudent {
    void study_hard() {
        //m_age = 100;    //m_age 不可访问,因为m_age本身是public的,但是通过GoodStudent私有继承,属性不可访问
        m_money = 10;  //m_age 本身是public,父类继承是保护继承,可访问
    }
};

总结:父类的成员变量或者成员函数是否能够访问与否,看这个成员变量本身的属性以及这个属性以什么样的方式继承过,访问权限取最小访问。

初始化列表

1.一种便捷的初始化成员变量的方式
2.只能用在构造函数中
3.初始化顺序只跟成员变量的声明顺序有关
4.初始化列表的参数可以是变量也可以是函数返回值

//一般的形式
 Person(int age,int height):m_age(age),m_height(height){}

int myAge(){ return 100;}
int myHeight() { return 200;}
//参数是函数返回值
Person(int age,int height):m_age(myAge()),m_height(myHeight()){}

初始化列表只跟成员变量的声明有关,如下所示:

//这里会先去初始化m_age,但是m_height此时没有初始化,m_age的值未知
Person(int age,int height):m_height(height),m_age(m_height){}
Person p(100,10);  //得到结果m_age = 未知 m_height = 100

我们再来看看构造函数的相互调用,构造函数相互调用可以通过初始化列表调用,在内部调用会创建临时对象。并不指向最外层调用对象。下面这种调用是错误的:

Person(){
        // 直接调用构造函数,会创建一个临时对象,传入一个临时的地址值给this指针
       Person(0, 0);
    }
Person(int age, int height) :m_age(age), m_height(height) { }

构造函数的正确调用如下:

 Person() :Person(10, 10) { }  //无参构造通过初始化列表调用有参的构造函数
 Person(int age, int height) :m_age(age), m_height(height) { }

 Person p;
 p.display();

初始化列表与默认参数的配合使用,如下所示:

Person(int age = 5, int height = 100) :m_age(age), m_height(height) { }

Person p1;  //没有参数,调用使用默认参数
Person p2(1); //age使用1初始化,height使用默认参数
Person p3(3,4); // age = 3  height = 4

请注意,如果类的声明和实现分离的,初始化列表只能写在函数的实现中,而默认参数只能写在函数声明中

struct Person {
    int m_age;
    int m_height;
    Person(int age = 5,int height = 10);  //函数声明中
};
Person::Person(int age,int height):m_age(age),m_height(height){}  //初始化列表在函数实现中

关于父类与子类的构造函数,有以下几点需要注意的:
子类的构造函数默认会调用父类的无参构造

struct Person {
    int m_age;
    int m_height;
    
    Person():Person(10,20){
        cout << "Person()" << endl;
    };
    Person(int age,int height):m_age(age),m_height(height){
        cout << "Person(int age,int height)" << endl;
    }

};

struct Student :public Person
{
    int m_score;
    //子类如果不显示调用父类构造函数,会默认调用父类的无参构造
    Student(int score):m_score(score){}
}

如果子类调用了父类的有参构造函数,则不会再调用无参构造函数。

Person(int age,int height):m_age(age),m_height(height){
        cout << "Person(int age,int height)" << endl;
}

//子类如果显示调用父类有参构造函数,则不会调用无惨构造函数
Student(int score):m_score(score),Person(10,20){}

如果父类只有有参构造,没有无参构造,子类必须显示调用父类的有参构造。构造函数与析构函数的顺序刚好相仿,构造函数是先从父类构造,再构造自己,而析构函数刚好相反。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 3. 类设计者工具 3.1 拷贝控制 五种函数拷贝构造函数拷贝赋值运算符移动构造函数移动赋值运算符析构函数拷贝和移...
    王侦阅读 1,870评论 0 1
  • 这是16年5月份编辑的一份比较杂乱适合自己观看的学习记录文档,今天18年5月份再次想写文章,发现简书还为我保存起的...
    Jenaral阅读 2,849评论 2 9
  • C++文件 例:从文件income. in中读入收入直到文件结束,并将收入和税金输出到文件tax. out。 检查...
    SeanC52111阅读 2,848评论 0 3
  • 一个博客,这个博客记录了他读这本书的笔记,总结得不错。《深度探索C++对象模型》笔记汇总 1. C++对象模型与内...
    Mr希灵阅读 5,638评论 0 13
  • 雨飘飘洒洒, 在路灯的映照下, 像是一朵朵散落的花儿。 就连落到地上的雨点, 也化成五彩缤纷的花瓣。 这场雨, 不...
    panjw阅读 262评论 0 4