关于c++:类和对象

3次阅读

共计 7248 个字符,预计需要花费 19 分钟才能阅读完成。

  • 类由两局部组成:成员变量(属性)、成员函数(成员的行为)
  • 在类中定义的成员函数,编译器会默认成内联函数。
  • C++ 中将 struct 降级为类的定义(同样可能定义构造体,兼容 C 语言)
  • C++ 中还能够应用 class 来定义类,用法跟 struct 雷同。

类的申明与实例化

struct People  // 还能够写为 class People
{
public:  // 示意下列的都是私有的
   void Init()  // 类中能够定义函数,构造体不行 
   {
       int a = 10;//Init 函数在类的外部创立作用域,所以这里 Init 函数是定义
   }
   void Push(); //Push 函数是在里面定义的,这里只是申明(申明和定义拆散)int _a;    // 这里的成员变量都是申明,不是定义,在内存中开拓空间时能力称为定义。char _b;
  struct People* _ptr;
}   

void People:: Push()
{int a = 10;}
int main()
{People man; // 类的实例化}

类的拜访限定符

  • public:私有
  • protected:爱护
  • private:公有
  • 拜访限定符的作用范畴:以后拜访限定符的地位到下一个拜访限定符的地位,或者类完结。
  • 如果没有增加拜访限定符,class 默认状况下是公有的,struct 默认状况下是私有的。

类的大小

  • 类中的函数被寄存在公共区域中(代码段)
  • 所以类的大小只思考成员变量的大小,不思考成员函数的大小。
  • 留神:思考成员变量大小时,遵循构造体的对齐规定。
  • 空类的大小为 1,给一个字节是为了占位,用来示意对象存在。
class People
{
    char _b;
    int _a;
    short _c;
    void init()
    {}}
int main()
{
    People man;
    cout << sizeof(man) << endl;  // 输入后果为 12 
}

面向对象的三大个性

  • 封装:将数据和操作数据的办法进行有机联合,暗藏对象的属性和实现细节,仅对外公开接口来和对象进行替换(其本质是一种治理)
  • 继承
  • 多态

this 指针

  • 在定义两个对象 today 和 tommor 时,调用 Init 函数,他们调用的是同一个函数。
  • 然而,这时 _year 等的成员变量就不好辨别到底是 today 的还是是 tommor 的。
  • 所以编译器就减少了一个隐含的参数

    • void Init(Date* this ,int year,int month,int day)
    • Date* this 这个参数用来传递对象的指针,表明以后是哪个对象。
  • Init 函数中的成员变量就会变成:this->_year = year;
  • 上述的都不必本人写,这是编译器自带的。
  • this 指针个别是存在栈上的。有可能存储在寄存器中(VS 就是存在寄存器中)
class Data
{
public:
    void Init(int year,int month,int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Data today;
    Data tommor;
    today.Init(2022, 7, 7);
    tommor.Init(2022, 7, 10);
    return 0;
}

类的六个默认成员函数

1、构造函数

  • 函数性能:实现对象的初始化(个别状况下须要本人写构造函数)
  • 特色:构造函数的函数名与类名雷同,无返回值,实例化对象时会主动调用,构造函数能够重载。
  • 留神:默认构造函数与构造函数是不同的,所谓的默认构造函数不只是指编译器主动生成的函数,而是指不必参数就能够调用的构造函数(无参构造函数和全缺省构造函数)

    • 当咱们不写默认构造函数时,编译器会主动生成。
    • 咱们本人写的无参数的构造函数。
    • 咱们本人写的全缺省的构造函数。
    • 默认构造函数只能有一个。
  • 留神:

    • 编译器主动生成的默认构造函数,不会初始化根本类型的变量(如 int char short 等),然而会初始化自定义类型(构造体、类等)
    • 编译器会去调用自定义类型的默认构造函数对自定义类型的变量进行初始化。
  • 理论中最举荐的是写一个全缺省的默认构造函数。

状况一:

  • Data(int year,int month,int day) 函数占用了默认构造函数名 Data(然而它不是默认构造函数),所以编译器不会再主动生成默认构造函数。

    • Data today(2022, 7, 15); // 这里是对的,因为创建对象时,传递参数了。
    • Data tomor; // 这里零碎会报错,创建对象时,没有传递参数,零碎会主动应用默认构造函数进行初始化,然而 Data 函数曾经占用了默认构造函数名,所以编译器没有主动生成,零碎会提醒“类 Data 不存在默认构造函数”
class Data    
{
public:
    Data(int year,int month,int day)  // 是构造函数,但不是默认构造函数
    {
        _year = year;
        _month = month;
        _day = day;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{Data today(2022, 7, 15);  // 实例化对象,有参数传递,主动调用构造函数,Data tomor;   // 实例化对象,没有参数,主动调用默认构造函数,默认构造函数不存在
    return 0;
}

状况二:

  • 将状况一的 Data 函数改为如下,零碎则不会报错。
  • 因为 Data(int year = 1,int month = 2,int day = 3) 函数是全缺省函数,被零碎认为是默认构造函数。
class Data
{
public:
    Data(int year = 1,int month = 2,int day = 3) // 全缺省构造函数
    {
        _year = year;
        _month = month;
        _day = day;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{Data today(2022, 7, 15);
    Data tomor;
    return 0;
}

状况三:

  • 同时定义 Data() 和 Data(int year ,int month ,int day) 函数,编译器会主动认为 Data() 是默认构造函数,编译器不会再主动生成。
class Data
{
public:
    Data(int year ,int month ,int day) // 构造函数,但不是默认构造函数
    {
        _year = year;
        _month = month;
        _day = day;
    }
    Data()  // 无参的构造函数,是默认构造函数
    {
        _year = 1;
        _month = 2;
        _day = 3;
    }

private:
    int _year;
    int _month;
    int _day;
};

int main()
{Data today(2022, 7, 15);  // 这里调用的是 Data(int year ,int month ,int day)
    Data tomor;  // 这里调用的是 Data()
    Data tomor(); // 这里不能这样写,编译器无奈确认是在定义一个函数,还是在调用默认构造函数
   return 0;
}

2、析构函数

  • 函数性能:析构函数不是用来销毁对象的,而是在对象销毁时,主动调用,实现类的资源清理工作。
  • 特色:

    • 析构函数的函数名是在类名前加一个 ~
    • 无参数,无返回值
    • 一个类只有一个析构函数,不能重载
    • 若程序员没有定义析构函数,则零碎会主动生成
    • 对象生命周期完结时,主动调用析构函数
  • 零碎主动生成的析构函数不会解决根本类型的成员,只会解决自定义类型的成员,会去调用自定义类型成员的析构函数。
  • 个别不必写析构函数的,只有在类中开拓空间了,才须要清理(free 开释空间)
class Data
{
public:
    Data(int year = 1 ,int month = 2 ,int day = 3) // 默认构造函数
    {
        _year = year;
        _month = month;
        _day = day;
    }
    ~Data()  // 析构函数
    {// 资源清理}

private:
    int _year;
    int _month;
    int _day;
};

多个对象的析构程序

  • 因为创建对象时会建设栈帧,所以构造函数和析构函数也会在栈中被建设,所以也要满足后进先出准则。
  • 所以,today 先结构,tomor 后结构;tomor 先析构,today 后析构。
int main()
{Data today(2022, 7, 15);  // 创建对象了,调用默认构造函数,对象销毁时会调用析构函数
    Data tomor;
    return 0;
}

3、拷贝构造函数

  • 函数性能:依据传入的对象,拷贝一个雷同的对象。
  • 留神:拷贝是拿一个曾经存在的对象去结构初始化另一个要创立的对象。
  • 拷贝构造函数是构造函数的重载模式,只有一个形参,这个形参类型是对本类类型对象的援用,个别会用 const 润饰这个援用参数。
  • 留神:拷贝构造函数的参数必须是援用传参,传值传参会引发无穷递归。

    • 传值传参时,形参是实参的拷贝,拷贝类的对象则会调用拷贝构造函数,如此循环。
  • 如果咱们没有写拷贝构造函数,编译器会主动生成拷贝构造函数

    • 然而这个拷贝构造函数会进行浅拷贝(值拷贝),即间接拷贝内存空间中的值到另一个空间,无论内部是根本类型还是自定义类型。
  • 留神:默认拷贝构造函数存在缺点

    • 在类中 malloc 开拓空间时,会有一个指针指向这个空间
    • 应用默认拷贝构造函数进行拷贝时,浅拷贝,只会将这个指针中存储的地址拷贝过来,而不是开拓新的空间
    • 所以拷贝实现后,两个对象中的指针都会指向同一内存空间
    • 而在对象销毁时,会调用析构函数开释 free 这个内存空间,调用两次析构函数,对同一内存空间开释了两次,这里会出错。(这时候就须要深拷贝)
Data::Data(const Data& d)  // 参数必须是援用传参,这是在类的函数。{
    _year = d._year;
    _month = d._month;
    _day = d._day;
}
int main()
{Data d1(2022, 7, 15); // 调用默认构造函数初始化。Data d2(d1);   // 调用拷贝构造函数,复制 d1 到 d2。return 0;
}

4、赋值运算符重载函数

  • 赋值运算符的重载函数也是一个默认成员函数,如果没有定义,编译器会主动生成。
  • 它跟拷贝构造函数类似,会对根本类型进行浅拷贝,对自定义类型会调用对方的赋值运算符重载进行拷贝。
  • 留神:赋值重载必须是两个曾经存在的对象间进赋值。
Data::Data operator=(const Data& x1)  
{
    _year += x1._year;
    _month += x1._month;
    _day += x1._day;
    return *this;  // 赋值运算符要有返回值
}
int main()
{Data d1(2022, 7, 15);
    Data d2(today);
   d1 = d2; // 相当于调用函数 operator=(d1,d2)Data d3 = d1; // 这里是调用的拷贝构造函数,用曾经存在的对象去初始化新建设的对象
    return 0;
}

const 润饰成员函数

  • 在定义类成员函数时,在开端加上 const,示意 *this 被 const 润饰。
  • 如果 *this 不小心被批改,则会被编译器检测进去。
bool Data::operator==(const Data& d)const
{return (_year == d._year) && (_month == d._month) && (_day == d._day);
}
  • 留神:

    • const 对象不能够调用非 const 成员函数
    • 非 const 对象能够调用 const 成员函数
    • const 成员函数不能够调用非 const 成员函数
    • 非 const 成员函数能够调用 const 成员函数

取地址符重载函数

  • 编译器会默认生成,没啥价值。
Data* Data::operator&()
{return this;}
const Data* Data::operator&()
{return this;}

友元函数 friend

  • 当某个函数须要写成全局函数,但又同时心愿可能调用类中的公有成员时,能够应用有元函数。
  • 申明有元函数时,必须在类中申明(能够是类中的任何地位,不肯定要在 private 之前)
  • 友元关系是单向性的

    • 例如在 Data 类中申明 Time 类为友元类,则 Time 类能够间接拜访 Data 类的公有成员,然而 Data 类不能拜访 Time 类的公有成员。
  • 友元关系不能传递

    • B 是 A 的有元,C 是 B 的友元,然而不能阐明 C 是 A 的友元。
  • 个别状况不倡议应用友元函数,它会毁坏封装。
class Data
{friend int Add(const Data& a1, const Data& a2);// 友元函数
private:
    int _year;
    int _month;
    int _day;
};

// 全局函数实践上是不能调用_day 等公有成员的,然而在类中申明成友元函数就能够
int Add(const Data& a1, const Data& a2)
{return a1._day + a2._day;}

运算符重载函数 operator

  • 函数性能:为了让自定义类型可能像根本类型一样进行运算(例如类对象的加减等)
  • 留神:
  • 不能创立新的运算符。
  • (.*)、(: :)、(sizeof)、(? :)、(.)以上 5 个运算符不能进行重载。
 函数原型:返回值类型  operator 操作符 (参数列表)
{
}

// 这个重载函数是在类中的,存在默认参数 this(this 是指针类型),所以只须要写一个参数。// 如果写的全局的函数,则须要写两个参数。// 类的加法,第一个参数是默认的 this 参数代表运算符的左值,第二个参数 x1 是运算符右值
void Data::operator+=(const Data& x1)  
{
    _year += x1._year;
    _month += x1._month;
    _day += x1._day;
}
int main()
{Data today(2022, 7, 15);
    Data tomor(today);
    today += tomor;   // 相当于调用函数 operator+(today,tomor)return 0;
}

前置 ++ 和后置 ++

  • 前置 ++ 和后置 ++,因为运算符号都是 ++,所以函数名也是雷同的 operator++()
  • 在 C ++ 编译器中,对后置 ++ 做了非凡解决

    • 后置 ++ 的重载函数中,默认会传递一个 int 类型参数,并不必传递理论参数,只是用来示意它是后置 ++。
int Data::operator++() // 前置 ++
{
    this->_day += 1;
    return this->_day;
}

int Data::operator++(int a) // 后置 ++ 
{
    this->_day += 1;
    return this->_day - 1;
}

int main()
{Data today(2022, 7, 15);
    cout << today++ << endl;  // 输入为 15 // 相当于调用 operator++(&today,0)
    cout << ++today << endl;  // 输入为 17 // 相当于调用 operator++(&today)
    return 0;
}

构造函数的初始化

初始化办法一:函数体内初始化

Data(int year,int month,int day)  
{
    _year = year;
    _month = month;
    _day = day;
}

初始化办法二:初始化列表初始化

  • 以冒号开始,两头用逗号隔开,每个成员变量前面跟一个小括号,括号中为初始化值。
  • 每个成员变量在初始化列表中只能初始化一次。
  • 个别倡议应用初始化列表进行初始化。
  • 留神:

    • const 润饰的成员、援用变量、没有默认构造函数的自定义类型成员等必须应用初始化列表中初始化(这些变量必须在定义时就初始化,然而类中只是申明,创建对象时才是定义)
  • 成员变量在类中的申明程序就是初始化程序,与列表中的先后顺序无关。

    • 例如:假如下列申明程序为 _day、_month、_year,那么初始化时的程序就是这个程序,而不是 _year、_month、_day。
Data(int year,int month,int day)   // 初始化列表
    :_year(year)
    ,_month(month)
    ,_day(day)
{// 函数体}

创立类的对象的办法

  • 对于单个成员变量时,能够应用一下三种创建对象办法
  • 第一种是间接调用构造函数,定义有名对象。
  • 第二种是隐式类型转换,创立长期对象 Math b(10),而后在拷贝给 a。第一种和第二种在系统优化的状况下是一样的。
  • 第三种办法是结构匿名对象,该对象的生命周期只在那一行,那一行完结后,该对象就被销毁。

    • 实用场景:要应用类中某个函数,而且只在这一行应用,然而定义有名对象比拟麻烦,应用匿名对象会比拟快捷。
class Math
{
public:
    Math(int a)
        :_a(a)
    { }
    int sum()
    {return _a+10;}
private:
    int _a;

};


int main()
{Math a1(10);
   a1.sum();     // 应用有名对象调用函数
    Math a2 = 10;
    Math(10);
    cout << Math(5).sum() << endl; // 应用匿名对象调用函数
    return 0;
}

动态成员

动态成员变量

  • 动态成员,寄存在动态区,它属于整个类,也属于类的所有对象,不属于某一个对象。
  • 动态成员变量不能在构造函数中初始化,它只能在函数外进行初始(相当于全局变量)
  • 如果想要拜访这个动态变量,个别是建设私有的成员函数,通过调用这个函数来进行拜访。
class A
{
public:
    A()
    {++_n;}
    static int Getn1()  // 动态成员函数
    {
            a = 10; //Getn1 是动态成员函数,a 是非动态成员变量,这里会报错
        return _n;
    }
   int Getn2()
    {return _n;}
private:
    static int _n;  // 这里只是申明。int a;
};

int A::_n = 0;   // 这个不能写在 main 函数中

int main()
{    
    A a1;
    cout << a1.Getn1() << endl;   
    cout << A().Getn2() << endl;
   cout << A::Getn1() << endl;   //Getn1 是动态成员函数,不能应用这种办法
   cout << A::Getn2() << endl;  
    return 0;
    
}

动态成员函数

  • 跟一般成员函数的区别:它没有 this 指针,不能拜访非动态成员,只能拜访动态成员。
  • 例如上述中的 Getn1 函数就是动态成员函数。
正文完
 0