• 类由两部分组成:成员变量(属性)、成员函数(成员的行为)
  • 在类中定义的成员函数,编译器会默认成内联函数。
  • 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函数就是静态成员函数。

夜枫微凉
27 声望4 粉丝

« 上一篇
C++入门
下一篇 »
C++内存管理