C++类与对象

1. 类的引入与定义

1.1 类的引入

c语言中,学习了结构体,但是此时的结构体中只能定义变量。而在c++中,结构体中不仅可以定义变量,还可以定义函数
在c++中更常用class来代替结构体。

1.2 类的定义

class Person//Person为类的名字
{
  //大括号内为类体,类体由成员函数和成员变量组成。
};//最后一定不要少了分号。

在上述代码中,class为定义类的关键字,Person为类的名字,{}内为类的主体。类中的元素称为类的成员, 类中的数据称为成员标量或者类的属性,类中的函数称为成员函数或者类的方法
类的定义有两种方式:

  1. 声明和定义都在类体中
class Person//Person为类的名字
{
   public:
       void show()
       {
           cout<<"名字"<<_name<<endl;
       }
   public:
       char _name;
       char _sex;
       int _age;
};
  1. 声明在.h文件中,定义在.cpp文件中。
//.h文件中
class Person
{
public:
  void show();
public:
  char _name;
  char _sex;
  int _age;
};
//.cpp文件中
#include"Person.h"

void Person::show()
{
   cout<<"名字"<<_name<<endl;
}
//一般工程量大的情况下,采用第二种方法较为方便。

1.3 类的访问限定符及封装

1.3.1 类的访问限定符

在class中,访问限定符共有三种:

  1. public(共有):修饰的成员在类外可以直接被访问
  2. protected(保护):类外不可直接访问
  3. private(私有):类外不可直接访问
    class和struct不同的是class的默认访问权限为private,struct的默认访问权限是public。

1.3.2 封装

面对对象的三大特性:封装、继承、多态。
封装就是将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。

1.4 类的作用域

以第二种类定义的方式来说,当在.h文件中声明类之后,在.cpp文件中定义函数时(类外定义),需要在函数名前加上==::==作用域解析符指明成员属于哪个类。

//.h文件中
class Person
{
public:
  void show();
public:
  char _name;
  char _sex;
  int _age;
};
//.cpp文件中
#include"Person.h"

void Person::show()
{
   cout<<"名字"<<_name<<endl;
}

1.5 类的实例化

类就相当于模板,模板里边有固定的成员,定义类并没有给其分配实际的内存空间。而当对类进行实例化时,实例化出的对象是占有实际的内存空间的,用来存储类成员变量。(一个类可以实例化出很多个对象)

class Person
{
public:
    void show();
private:
    char _name;
    char _sex;
    int _age;
}

int main()
{
   Person p1;//Person类实例化出的对象p1
   Person p2;//Person类实例化出的对象p2
   //.......还可以实例化出无数个对象
   return 0;
}

1.6 类对象模型

1.6.1 类对象的大小

在c++中,成员变量和成员函数是分开存储的,只有非静态成员变量才属于类的对象上,而静态成员变量不占用对象空间,成员函数和静态成员函数也不占用对象空间。

#include<iostream>
using namespace std;

class Person
{
public:
    Person()
    { 
        _age = 0;
    }
     int _age;
    static char _age2;
    void fun()
    {
        cout << "成员函数" << endl;
    }
    static void func()
    {
        cout << "静态成员函数" << endl;
    }
};

int main()
{
    cout << "Person的大小为" << sizeof(Person) <<endl;
    return 0;
}

此程序为计算类大小的程序,运行结果如下:
在这里插入图片描述
此结果也可表明,只有非静态成员变量才属于类的对象上。成员函数则会存放在公共的代码段。需要注意的一个问题是内存对齐

1.6.1.1 结构体内存对齐规则
  1. 第一个成员在与结构体偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    注意:对齐数=编译器默认的一个对齐数与该成员大小的较小值(vscode默认对齐数为8)。
  3. 结构体总大小为:最大对齐数(所有变量类型的最大者与默认对齐参数取最小)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
    如果定义两个char类型的成员变量以及一个int类型的成员变量,由于vscode默认对其数那么用sizeof计算出的大小为8。
#include<iostream>
using namespace std;

class Person
{
public:
    Person()
    { 
        _age = 0;
    }
     int _age;
     char _name;
     char _sex;
};

int main()
{
    cout << "Person的大小为" << sizeof(Person) <<endl;
    return 0;
}

运行结果为:
在这里插入图片描述
由此可以看出,编译器在编译时,自动遵循结构体内存对齐规则。

2. this指针

2.1 this指针的引出

先定义一个Person类

#include<iostream>
using namespace std;
#include<string.h>

class Person 
{
public:
    void display()
    {
        cout << "姓名:" << _name << "\t" << "年龄:" << _age <<endl;
    }
    void setperson(string name , int age)
    {
        _name = name ;
        _age = age;
    }
private:
    string _name;
    int _age;
};

int main()
{
    Person p1,p2;
    p1.setperson("张三",18);
    p2.setperson("李四",19);
    p1.display();
    p2.display();
    return 0;
}

在上述程序中,有一个问题。在main函数中,有两个Person的实例化对象p1和p2。当调用类中的setperson函数时,此函数是怎么区分设置p1对象还是设置p2对象。
在c++中是通过引入this指针来解决这一问题的。c++在编译时,会对每一个“非静态成员函数”增加一个隐藏的指针参数(不可以显式定义this指针),让这个指针指向当前的对象。例如:

p1.setperson("张三",18);
//则在此成员函数调用时,this指针会指向p1

此过程是编译器自动完成,不需要用户传递。

2.2 this指针的特性

  1. this指针的类型:*const
  2. this指针只能在成员内部使用
  3. this指针本质上其实是一个成员函数的形参,在对象调用函数时,将对象地址作为实参传递给this形参,所以对象中不存储this指针。
    编译器处理成员函数隐含的this指针是以下列方式进行处理:
void display()
{
  cout << _name <<endl;
}//这是成员函数
void display(Person *this)
{
  cout << this->_name <<endl;
}//这是编译器自动做的处理

面试问题:

  1. this指针存放在哪里?
    答:存放在栈中。
  2. this指针可以为空吗?
    答:this指针可以为空,但是为空的时候,不可以访问成员变量,否则会报错。

3. 类的默认成员函数

3.1 类共有6个默认成员函数

class Person{};

如上代码所示,当定义一个类,但是类内什么都没有,即为空类。空类中什么都没有,但是都会自动生成6个默认成员函数

  1. 构造函数:主要完成初始化工作。
  2. 析构函数:主要完成清理工作。
  3. 拷贝构造:使用同类对象初始化创建对象。
  4. 赋值重载:把一个对象赋值给另一个对象。
  5. 普通对象取地址重载(很少自己实现)
  6. const对象取地址(很少自己实现)

3.2 构造函数

3.2.1 构造函数的引入

以Person类为例

#include<iostream>
using namespace std;
#include<string.h>

class Person 
{
public:
    void display()
    {
        cout << "姓名:" << _name << "\t" << "年龄:" << _age <<endl;
    }
    void setperson(string name , int age)
    {
        _name = name ;
        _age = age;
    }
private:
    string _name;
    int _age;
};

int main()
{
    Person p1,p2;
    p1.setperson("张三",18);
    p2.setperson("李四",19);
    p1.display();
    p2.display();
    return 0;
}

由上述代码可以看出,如果想设定某人的姓名及年龄可以通过setperson函数来实现,但是每次创建对象都要调用该方法设置信息,较为麻烦,所以就想到可不可以在创建对象时,将信息直接设置?由此就引出了构造函数。
构造函数是一个特殊的成员函数名字与类名相同,当创建类对象时,编译器会自动调用,保证每个数据成员都有一个合适的初始值,并且在此对象的生命周期内只调用一次。

3.2.2 构造函数的特性

  1. 函数名与类名相同。
  2. 无返回值。
  3. 类进行实例化对象时编译器自动调用对应的构造函数。
  4. 构造函数可以重载。
  5. 当我们不提供构造函数时,会进行空实现,即编译器会自动调用一个无参的默认构造函数,当定义了构造函数后,编译器将不会自动生成。
    需要注意的是:构造函数主要的任务并不是开辟内存空间创建对象,而是初始化对象。
    还有一点需要注意的是:无参构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。无参构造函数、全缺省构造函数、没写编译器默认生成的构造函数,都可以认为是默认成员函数。
#include<iostream>
using namespace std;
#include<string.h>

class Person
{
public: 
    Person()
    {
      cout << "调用无参构造函数" << endl;
    }
    Person(string name,int age)
    {
        _name = name;
        _age = age;
        cout << "调用有参构造函数" << endl;
    }
private:
     string _name;
     int _age;
};

int main()
{
  Person p1;//调用无参构造函数
  Person p2("张三"18);//调用有参构造函数
   return 0 ;
}

运行结果如下图所示:
在这里插入图片描述

3.2.3 构造函数的调用规则

  1. 如果用户定义有参构造函数,则c++不再提供默认无参构造函数,但是会提供默认拷贝构造。
  2. 如果用户定义拷贝构造函数,c++不会再提供其他的构造函数。

3.3 析构函数

3.3.1 析构函数的概念

析构函数和构造函数的功能正好相反,析构函数是对象在销毁时会自动调用析构函数,完成类的资源清理工作。

3.3.2 析构函数的特性

  1. 析构函数名是在类名前加上字符~。
class Person
{
   ~Person()//析构函数
   {
   }
};
  1. 无参数无返回值。
  2. 一个类有且只有一个析构函数,如果用户没有定义,则系统会自动生成默认的构造函数。
  3. 类实例化的对象生命周期结束时,c++编译器会自动调用析构函数。
#include<iostream>
using namespace std;
#include<string.h>

class Person
{
public: 
    Person(string name,int age)
    {
        _name = name;
        _age = age;
        cout << "调用有参构造函数" << endl;
    }
    ~Person()
    {
        cout << "调用析构函数" << endl;
    }
private:
    string _name;
    int _age;
};

int main()
{
  Person p1("张三",18);//调用有参构造函数
   return 0 ;
}

此代码运行结果为:
在这里插入图片描述
由上图运行结果可得,当此类对象结束时,自动调用析构函数。

3.4 拷贝构造

3.4.1 拷贝构造的概念

在创建对象时,有时需要创建两个一模一样的对象,这是就要用到拷贝构造。
拷贝构造函数只有单个形参,该形参是对本类类型形参的引用(一般常用const修饰),用在已存在的类类型对象创建新对象时由编译器自动调用。

3.4.2 拷贝构造的特征

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数的参数只有一个并且必须使用引用传参如果使用传值方式会引发无穷递归调用
  3. 若用户没有自定义拷贝构造函数,系统会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝
    这里需要注意的是,当类内定义的变量不是指针变量时,浅拷贝就可以完成拷贝构造,但是如果类内定义了指针变量时,浅拷贝会出现报错,此时需要使用深拷贝。
    首先举一个浅拷贝的例子。
#include<iostream>
using namespace std;
#include<string.h>

class Person
{
public:
    Person(string name,string sex,int age)
    {
        _name = name;
        _sex = sex;
        _age = age;
    }
    Person(const Person& p)
    {
        _name = p._name;
        _sex = p._sex;
        _age = p._age;
    }
     string _name;
     string _sex;
     int _age;
};

int main()
{
    Person p1("张三","男",18);
    cout<< "p1的姓名:"<< p1._name<<'\t'<< "p1的性别:"<< p1._sex<<'\t'<< "p1的年龄:"<< p1._age<<endl;
    Person p2(p1);
    cout<< "p2的姓名:"<< p2._name<<'\t'<< "p2的性别:"<< p2._sex<<'\t'<< "p2的年龄:"<< p2._age<<endl;
    return 0;
}

此程序的运行结果即为函数的浅拷贝:
在这里插入图片描述

3.4.3 浅拷贝和深拷贝的区别

浅拷贝简单的复制拷贝操作
深拷贝在堆区重新申请空间,进行拷贝操作

接下来简单说一下深拷贝,如下述代码

#include<iostream>
using namespace std;
#include<string.h>

class Person
{
public:
    Person(string name,string sex,int age)
    {
        _name = name;
        _sex = sex;
        _name = age;
    }
    Person(const Person& p)
    {
        _name = p._name;
        _sex = p._sex;
        _age = p._age;
    }
     string _name;
     string _sex;
     int *_age;
};

int main()
{
    Person p1("张三","男",18);
    cout<< "p1的姓名:"<< p1._name<<'\t'<< "p1的性别:"<< p1._sex<<'\t'<< "p1的年龄:"<< p1._age<<endl;
    Person p2(p1);
    cout<< "p2的姓名:"<< p2._name<<'\t'<< "p2的性别:"<< p2._sex<<'\t'<< "p2的年龄:"<< p2._age<<endl;
    return 0;
}

在这里插入图片描述

以浅拷贝的方式进行操作的话,此程序运行会崩溃。原因是在定义类成员变量的时候,定义了一个int*类型的成员变量,给p1赋值之后,p1中的年龄指向内存中的一块空间,即存放着_age的内存空间,如果使用浅拷贝的话,p2直接拷贝p1,会出现p2的年龄也指向同一块内存空间,此时p1和p2的_age指针指向同一块内存空间当程序继续运行,执行析构函数时,先对p1中指针变量所指向的空间进行释放清理,当执行对p2中指针变量所指向的空间进行释放清理时,此内存空间已经被释放清理过一次了,不可以再次进行释放清理,所以程序会崩溃。解决方法就是采用深拷贝的方式完成拷贝构造。

#include<iostream>
using namespace std;
#include<string.h>

class Person
{
public:
    Person(string name,string sex,int age)
    {
        _name = name;
        _sex = sex;
        _age = new int (age);
    }
    Person(const Person& p)
    {
        _name = p._name;
        _sex = p._sex;
        _age = new int (*p._age);
    }
    ~Person()
    {
        if(_age != NULL)
        {
            delete _age;
        }
    }
     string _name;
     string _sex;
     int *_age;
};

int main()
{
    Person p1("张三","男",18);
    cout<< "p1的姓名:"<< p1._name<<'\t'<< "p1的性别:"<< p1._sex<<'\t'<< "p1的年龄:"<< *p1._age<<endl;
    Person p2(p1);
    cout<< "p2的姓名:"<< p2._name<<'\t'<< "p2的性别:"<< p2._sex<<'\t'<< "p2的年龄:"<< *p2._age<<endl;
    return 0;
}

如上述代码所示,即为深拷贝,在拷贝时,在堆区创建新的空间存放拷贝的数据,让指针指向新的内存空间,这样执行析构函数时就不会出现程序崩溃的情况。(具体后续再总结)

4. 赋值运算符重载

4.1 运算符重载

运算符重载是具有特殊函数名的函数。
函数名字为:关键字operator后边接需要重载的运算符符号。
函数原型:返回类型 operator操作符(参数列表)
需要注意的是:

  1. 不能通过连接其他符号来创建新的操作符:比如operator@
  2. 重载运算符必须有一个类类型或者枚举类型的操作数
  3. 用于内置类型的操作运算符,其含义不能改变,例如:内置的整型+,不能改变其含义。
  4. 作为类成员的重载函数时,其形参看起来比操作数数目少一个,其实是成员函数的操作符有一个默认的形参this,限定为第一个形参。
  5. .*(调用成员函数的指针) 、::(域作用限定符) 、sizeof(计算变量所占内存的大小) 、?:(三目运算符) 、.(结构体变量引用符) 以上五个运算符不能重载。

以下程序重载运算符==

#include<iostream>
using namespace std;
#include<string.h>

class Person
{
public:
    Person(string name,string sex,int age)
    {
         _name = name;
         _sex = sex;
         _age = age;
    }

    bool operator==(const Person& p)
    {
        return _age == p._age;
    }

    string _name;
    string _sex;
    int _age;
};

int main()
{
    Person p1("张三","男",19);
    Person p2("李四","男",19);
    if(p1 == p2)
    {
        cout << p1._name << "和" << p2._name << "年龄相同" <<endl;
    }
    else
    {
        cout << p1._name << "和" << p2._name << "年龄不相同" <<endl;
    }
    return 0;
}

此时运行结果为:
在这里插入图片描述

#include<iostream>
using namespace std;
#include<string.h>

class Person
{
public:
    Person(string name,string sex,int age)
    {
         _name = name;
         _sex = sex;
         _age = age;
    }

    bool operator==(const Person& p)
    {
        return _age == p._age;
    }

    string _name;
    string _sex;
    int _age;
};

int main()
{
    Person p1("张三","男",18);
    Person p2("李四","男",19);
    if(p1 == p2)
    {
        cout << p1._name << "和" << p2._name << "年龄相同" <<endl;
    }
    else
    {
        cout << p1._name << "和" << p2._name << "年龄不相同" <<endl;
    }
    return 0;
}

此时的运行结果为:
在这里插入图片描述
可见此时实现了==运算符的重载。

4.2 赋值运算符重载

#include<iostream>
using namespace std;
#include<string.h>

class Person
{
public:
    Person()
    {

    }
    Person(string name,string sex,int age)
    {
        _name = name ;
        _sex = sex ;
        _age = age ;
    }
    Person& operator=(const Person& p)
    {
        if(this != &p)
        {
            _name = p._name;
            _sex = p._sex;
            _age = p._age;
        }
    }
    string _name;
    string _sex;
    int _age;
};

int main ()
{
    Person p1("张三","男",18);
    Person p2;
    p2 = p1;
    cout<< "p2的姓名:"<< p2._name<<'\t'<< "p2的性别:"<< p2._sex<<'\t'<< "p2的年龄:"<< p2._age<<endl;
    return 0;
}

此时运行结果为:
在这里插入图片描述
可以看出完成了赋值运算符的功能,但是要区分开拷贝构造和赋值运算符。拷贝构造函数用于创建新对象并初始化它,而赋值运算符用于修改已存在对象的值。

赋值运算符需要注意四点:

  1. 参数类型
  2. 返回值
  3. 检测是否自己给自己赋值
  4. 返回*this

5. const成员

5.1 const修饰类的成员函数

先看下列代码:

#include<iostream>
using namespace std;
#include<string.h>

class Person
{
public:
     Person(string name,string sex, int age)
     {
        _name = name;
        _sex = sex;
        _age = age;
     }
     void Show()
     {
         cout<<"姓名:"<<_name<<'\t'<<"性别:"<<_sex<<'\t'<<"年龄:"<<_age<<endl;
     }
private:
     string _name;
     string _sex;
     int _age;
};

void func(Person p)
{
    p.Show();
}

int main()
{
    Person p1("李四","男",19);
    func(p1);
    return 0;
}

在上述代码中,虽然可以正常运行,但是当定义了一个Person类的p1后,调用func函数,如果传值的话,需要先拷贝一份,再进行打印出来,会浪费不必要的空间,所以func函数传参时可以进行传引用,就不需要再拷贝一份了。当传引用时,如果所传的值不需要发生改变的话,一般来说在前面都要加上const,如下所示,为修改成传引用。

void func(const Person& p)
{
    p.Show();
}

如果将代码改成传引用,其他地方不进行修改的情况下,会出现报错。我们先看一下报错的原因是什么
在这里插入图片描述
这个时候我们就要提到一个知识点。

c++允许权限的缩小,但是禁止权限放大,非const引用不能指向const对象,但是反过来const引用可以指向非const对象。

在这里插入图片描述
当编译器调用func函数时,会将p的地址传给p.Show();即实际上为p.Show(&d);,在前边我们说过,类成员函数中会有一个隐含的this指针,将d的地址传入,该指向d的this指针的类型为const Person*,而我们在定义类的时候,隐含的this指针的类型为Person*
在这里插入图片描述
所以,此时const Person *引用Person *,属于权限放大,所以会出现报错。
此时应该在void Show()函数中也加入const修饰,但是现在又出现了一个问题,我们在之前学习的时候已经替代了,this指针是隐式的,不可以显式定义,所以此时就引出了这样一种定义。

void Show() const//在Show()后加const 就相当于  void Show(const Person* this)
{
    cout<<"姓名:"<<_name<<'\t'<<"性别:"<<_sex<<'\t'<<"年龄:"<<_age<<endl;
}

即此时是这样的
在这里插入图片描述

可以看到运行结果正常输出
在这里插入图片描述
所以,在使用const时,一定要注意权限的缩小放大。

接下来还需要注意区分的几个内容:

  1. const Person* p1 ->const修饰*p1(指向的对象)
  2. Person const* p2 ->const修饰*p2(指向的对象)
  3. Person* const p3 ->const修饰p3(指针本身)
  4. 第一点和第二点没区别,一般来说都是写第一种形式。

同样的,对于成员函数之间也是这样的

func1()
{}
func2() const
{}
//此时只能func1()调用func2()  属于权限缩小
//func2()不可以调用func1()  属于权限放大

总结以上内容:

  1. const对象可以调用非const成员函数吗?
    不可以
  2. 非const对象可以调用const成员函数吗?
    可以
  3. const成员函数内可以调用其它的非const成员函数吗?
    不可以
  4. 非const成员函数内可以调用其它的const成员函数吗?
    可以

6. 成员初始化

我们在前边创建有参构造函数后,创建对象时的成员赋了一次值,但是这并不是初始化

class Person
{
public:
    Person(string name ,string sex, int age)
     {
        _name = name;
        _sex = sex;
        _age = age;
     }
private:
     string _name;
     string _sex;
     int _age;
};
int main()
{
    Person p1("李四","男",19);//此时并非初始化
    return 0;
}

构造函数体中的语句只能称为是赋初值。因为初始化只能初始化一次,而构造函数体内可以多次赋值。而且如果出现引用或者const赋初值就不成立,所以此时就需要用到初始化。

6.1 初始化列表

先说一下初始化列表的格式,以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。,代码如下所示

class Person
{
public:
    Person(string name ,string sex, int age)
         :_name(name)
         ,_sex(sex)
         ,_age(age)
     {
     }
private:
     string _name;
     string _sex;
     int _age;
};

需要注意以下几点:

  1. 每个成员变量初始化只能初始化一次(可以多次赋值)
  2. 类中包含(①引用成员变量②const成员变量③自定义类型成员即没有默认构造函数)这三类必须要放在初始化列表位置进行初始化。
class A
{
public:
   A(int a)
      :_a(a)
      {}
private:
     int _a ;
};

class B
{
public:
   B(int a,int ref)
      :_aobj(a)
      ,_ref(ref)
      ,_n(10)
      {}
private:
   A _aobj;       //没有默认构造函数
   int& _ref;     //引用
   const int _n;  //const
};
  1. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
#include<iostream>
using namespace std;

class A
{
public:
    A(int a1,int a2)
      :_a1(a1)
      ,_a2(_a1)
      {}
    void Show()
    {
        cout << _a1 << '\t' << _a2 <<endl;
    }
private:
     int _a2;
     int _a1;
};

int main()
{
    A A1(10,20);
    A1.Show();
    return 0;
}

例如上述程序,在声明时,先声明的_a2后声明的_a1,但是在初始化的时候,列表顺序时_a1在前,_a2在后。我们看一下运行结果。
在这里插入图片描述
_a2的值为0,_a1的值为10,可以看出,初始化的顺序并非初始化列表的顺序,而是声明的顺序。

6.2 explicit关键字

#include<iostream>
using namespace std;
#include<string.h>

class Person
{
public:
    Person(string name = "张三", string sex = "男" , int age = 18)
          :_name(name)
          ,_sex(sex)
          ,_age(age)
          {}
    void Show()
    {
        cout << _name << '\t' << _sex << '\t' << _age <<endl;
    }
private:
     string _name;
     string _sex;
     int _age;
};

int main ()
{
    Person p1("张三","男",18);
    p1.Show();
    p1 = {"李四","男",19};
    p1.Show();
    return 0;
}

如上所示代码中,可以对p1直接进行赋值。运行结果如下:
在这里插入图片描述
实际编译器会用{}内的值构造一个无名对象,最后用无名对象给p1对象进行赋值。如果在默认构造函数前加上explicit修饰,将会禁止对p1赋值这样的隐式转换。
在这里插入图片描述

6.3 成员赋值新方法

在目前c++中,支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量缺省值

#include<iostream>
using namespace std;
#include<string.h>

class Person
{
public:
    Person(){}
    void show()
    {
        cout << _name << '\t' << _sex << '\t' << _age <<endl;
    }
private:
    string _name = "张三";
    string _sex = "男";
    int _age  =18 ;
};

int main()
{
    Person p;
    p.show();
    return 0;
}

此程序运行结果为:
在这里插入图片描述

7. static成员

7.1 static成员的概念

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称为静态成员变量

7.2 特点

  1. 静态成员为所有类对象共享,不属于某个具体的对象。
  2. 静态成员变量必须在类外定义,定义时不添加static关键字。
  3. 类静态成员即可用类名::静态成员或者对象.静态成员来访问
  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员。
  5. 静态成员和类的普通成员一样,也有public、protected、private 3种访问级别,也可以具有返回值。

面试题:实现一个类,计算程序中创建出了几个类对象。

#include<iostream>
using namespace std;

class A
{
public:
    A()
    {
        ++_count;
    }
    A(const A& t)
    {
        ++_count;
    }
    static int Getcount()
    {
        return _count;
    }
private:
    static int _count;
};
int A::_count = 0;
int main()
{
    cout<<A::Getcount()<<endl;
    A a1, a2;
    cout<<A::Getcount()<<endl;
    A a3(a1);
    cout<<A::Getcount()<<endl;
    return 0;
}

此程序的运行结果为:
在这里插入图片描述问题

  1. 静态成员函数可以调用非静态成员函数吗?
    不能//因为静态成员函数中没有this指针
  2. 非静态成员函数可以调用类的静态成员函数吗?
    不能

8. 友元

友元分为:友元函数和友元类
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

8.1 友元函数

下边举一个必须用到友元函数的例子。
我们在上边重载了很多运算符,接下来我们试一下重载左移运算符(即operator<<)。先按照重载其他运算符一样,试一下重载左移运算符。

#include<iostream>
using namespace std;
#include<string.h>

class Person
{
public:
    Person(string name ,string sex, int age)
        : _name(name)
        , _sex(sex)
        , _age(age)
        {}
    void operator<<(ostream & out)
    {
        out <<_name <<endl;
    }
private:
     string _name;
     string _sex;
     int _age;
};

int main()
{
    Person p("张三","男",18);
    p << cout;
    return 0;
}

当程序是这样时,运行结果为:
在这里插入图片描述
可以看出,这时就把张三给输出了,但是一个问题是,我们使用的输入输出都是cout或者cin在前,想要输出的后,而在此程序中,反过来了降低了代码的可读性。如果把参数的部分改成对象(this指针就没办法指向对象),又不能成功输出,所以此时,我们只能在类外实现。因为类外实现的话,我们要传进去两个参数,不像在类内实现,只需要传一个参数,另外一个用隐含的this指针代替。

#include<iostream>
using namespace std;
#include<string.h>

class Person
{
public:
    Person(string name ,string sex, int age)
        : _name(name)
        , _sex(sex)
        , _age(age)
        {}
private:
     string _name;
     string _sex;
     int _age;
};

void operator<<(ostream & out,const Person& p)
{
    out <<_name <<endl;
}

int main()
{
    Person p("张三","男",18);
    cout << p;
    return 0;
}

我们把此重载函数放在类外之后,会发现一个问题,在类外无法访问_name,所以,这个时候就要用到友元了。友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。

#include<iostream>
using namespace std;
#include<string.h>

class Person
{
friend void operator<<(ostream & out,const Person& p);
public:
    Person(string name ,string sex, int age)
        : _name(name)
        , _sex(sex)
        , _age(age)
        {}
private:
     string _name;
     string _sex;
     int _age;
};
void operator<<(ostream & out,const Person& p)
{
    out <<p._name <<endl;
}
int main()
{
    Person p("张三","男",18);
    cout << p;
    return 0;
}

此时的运行结果为:
在这里插入图片描述
可以看到,此时就正常输出姓名了,但是还是有一个问题,这样的话,只能一个一个输出,不能像之前那样全部输出,这时候就需要在返回值处修改一下,将空返回改成引用返回,这样的话在输入一个<<即可接着输出。

#include<iostream>
using namespace std;
#include<string.h>

class Person
{
friend ostream& operator<<(ostream& out,const Person& p);
public:
    Person(string name ,string sex, int age)
        : _name(name)
        , _sex(sex)
        , _age(age)
        {}
private:
     string _name;
     string _sex;
     int _age;
};
ostream& operator<<(ostream& out,const Person& p)
{
    out <<p._name << '\t' << p._sex << '\t' <<p._age;
    return out;
}
int main()
{
    Person p("张三","男",18);
    cout << p <<endl;
    return 0;
}

此时的运行结果为:
在这里插入图片描述
可见此时,就完成了正常的输出。cin也是同样的道理。
注意:

  1. 友元函数可访问类的私有和保护成员,但不是类的成员函数。
  2. 友元函数不能用const修饰。
  3. 友元函数可以在类定义的任何地方声明,不受访问限定符的限制
  4. 一个函数可以是多个类的友元函数。
  5. 友元函数的调用与普通函数的调用和原理相同。

8.2 友元类

友元类的所有成员函数可以是另外一个类的友元函数,都可以访问另一个类中的非公有成员。
需要注意两点:

  1. 友元关系时单向的,不具有双向性。
  2. 友元关系不能传递。
class Date; // 前置声明
class Time
{
 friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有员变量,反过来,Time类不可以访问Date类中的私有成员
public:
 Time(int hour, int minute, int second)
 : _hour(hour)
 , _minute(minute)
 , _second(second)
 {}
 
private:
 int _hour;
 int _minute;
 int _second;
};
class Date
{
public:
 Date(int year = 1900, int month = 1, int day = 1)
 : _year(year)
 , _month(month)
 , _day(day)
 {}
 
 void SetTimeOfDate(int hour, int minute, int second)
 {
 // 直接访问时间类私有的成员变量
 _t._hour = hour;
 _t._minute = minute;
 _t.second = second;
 }
 
private:
 int _year;
 int _month;
 int _day;
 Time _t;
};

9. 内部类

在一个类中定义另一个类,这个内部的类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。而内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员但是外部类不是内部类的友元
特点:

  1. 内部类可以定义在外部类的public、protected、private都是可以的。
  2. 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
  3. sizeof(外部类)=外部类,和内部类没有任何关系。
class A
{
private:
		 static int k;
		 int h;
public:
		 class B
		 {
		 public:
				 void foo(const A& a)
				 {
				 cout << k << endl;//OK
				 cout << a.h << endl;//OK
				 }
		 };
};
int A::k = 1;
int main()
{
	 A::B b;
	 b.foo(A());
	 return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

雪好像下整夜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值