【C++入门基础】类和对象第二弹之类的认识并深入理解默认成员函数 (类和对象最全分享,默认成员函数、运算符重载、const成员及友元)

类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类。

空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

这六个默认成员函数,特殊点非常多,后面会一一学习,如果我们不实现,编译器会自己生成一份。

6个默认成员函数
初始化和清理
拷贝复制
取地址重载
构造函数主要完成初始化工作
析构函数主要完成清理工作
拷贝构造是使用同类对象初始化创建对象
赋值重载主要是把一个对象赋值给另外一个对象
主要是普通对象和const对象取地址/这两个很少会自己实现

构造函数

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象

其特征如下:

  1. 函数名与类名相同。

  2. 无返回值。

  3. 对象实例化时编译器自动调用对应的构造函数。

    ->防止你定义了对象但是没有初始化。

  4. 构造函数可以重载

  5. 如果类中没有显式定义构造函数,则c++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义,编译器将不再生成。

我们自己写一个Date的构造函数:首先创建Date类。我门之后创建的默认函数也是按照Date来创建的。

class Date
{
    private:
        int _year;
        int _month;
        int _date;
};
int main()
{
    Date d1;
    return 0;
}
class Date
{
public:
    Date()
    {
        _year = 0;
        _month = 1;
        _date = 1;
    }
    Date(int year,int month,int date)
    {
        _year = year;
        _month = month;
        _date = date;
    }

private:
    int _year;
    int _month;
    int _date;
};
int main()
{
    Date d1;//无参数初始化d1
    Date d2(2022, 10, 16);//有参数初始化d2
    return 0;
}

当然,我们可以用全缺省,来用一个函数代替两个函数的。推荐实现全缺省/半缺省。但是需要注意,语法上无参函数和缺省函数可以同时存在,但一旦调用无参,会出现二义性,报错。

class Date
{
public:
  	//推荐实现全缺省/半缺省
    Date(int year = 0,int month = 1,int date = 1)
    {
        _year = year;
        _month = month;
        _date = date;
    }

private:
    int _year;
    int _month;
    int _date;
};
int main()
{
    Date d1;//无参数初始化d1
    Date d2(2022, 10, 16);//有参数初始化d2
  	Date d3(2022);//半缺省初始化d3 - 2022-1-1
    return 0;
}

注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
warning C4930: “Date d3(void) ”:未调用原型函数(是否是有意用变量定义的?)
Date d3( );

5.“如果类中没有显式定义构造函数,则c++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义,编译器将不再生成。”

默认构造函数就是不用参数就可以调用的构造函数:

  • 无参的(我们定义的)
  • 全缺省的(我们定义的)
  • 编译器默认生成的

三者中任何一个都可以。而自己写的带参数的或者半缺省的构造函数 (并非全缺省) 没有初始化自定义类型的话,就会造成编译器报错。

C++把类型分为两类:

  • 内置类型 (基本类型):int/char/double/指针/内置类型数组

  • 自定义类型:struct/class定义的类型

我们不写而编译器默认生成的构造函数,对于内置类型不做初始化处理;

对于自定义类型的成员变量会去调用它的默认构造函数(不用参数就可以调的)进行初始化,如果没有默认构造函数就会报错。

构造函数构造顺序:全局变量> 谁先定义谁构造

总结:C++编译器默认生成的构造函数写的不好,没有对内置类型成员变量和自定义类型成员变量做统一处理。它不处理内置类型成员变量,只处理自定义类型的成员变量。

析构函数

通过前面构造函数的学习,我们知道一个对象是怎么初始化的,那一个对象又是怎么销毁的呢?

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

析构函数是特殊的成员函数,其特征如下:

  1. 析构函数名是在类名前加上字符~
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
  5. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
    ~Date()
    {}//Date类没有资源需要清理,所以Date不实现析构函数都是可以的。

Date类不需要清理,不代表所有的类都不需要。比如Stack类。

class Stack
{
public:
    Stack(size_t capacity = 4)
    {
        _a = (int *)malloc(sizeof(int) * capacity);
        if(_a == nullptr)
        {
            perror("malloc");
            exit(-1);
        }
        _top = 0;
        _capacity = capacity;
    }
    ~Stack()
    {
        free(_a);
        _a = nullptr;
        _top = _capacity = 0;
    }

private:
    int *_a;
    size_t _capacity;
    size_t _top;
};
int main()
{
    Date d1;               //无参数初始化d1
    Date d2(2022, 10, 16); //有参数初始化d2

    Stack s1;
    Stack s2(10);
    
    return 0;
}

析构函数因为在结束作用域的时候会自己调用所以非常方便。而且需要注意的一点是,析构函数是先进后出的,也就是先调用析构函数来销毁s2,随后销毁s1。

析构函数析构顺序:局部变量先析构> 出作用域销毁局部的静态变量 = 全局变量 (全局静态变量)

同一个生命周期的是反着的,后定义的先析构。

如果我们不写析构函数,编译器默认生成的析构函数与构造函数类似:

  • 对于内置类型的成员变量不做处理;

  • 对于自定义类型成员变量会去调用它的析构函数,如果没定义,就会报错。

拷贝构造

在创建对象时,可否创建一个与已存在对象一模一样的新对象呢?

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

那么我们如果自己写一下拷贝构造函数,很多小伙伴会这么写:


错误:

    Date(Date d)
    {
        _year = d._year;
        _month = d._month;
        _date = d._date;
    }
...
  int main()
{
    Date d1(2022,10,16);
    Date d2(d1);
    return 0;
}

但这是非常错误的!编译器会直接报错,这是因为我们使用了传值传参。传值传参本身就是一个拷贝的过程,是把d1的值传到了函数中,函数中创建了临时变量d来接收d1的值。这本身就是调用了拷贝构造。而拷贝构造有需要先传参数,传值传参又是一个拷贝构造。所以会造成无穷递归,栈溢出导致错误。

我们可以认为,自定义类型的传值传参就是一个拷贝构造,因为自定义类型的对象d用一个同类型对象d1初始化d本身,就是拷贝构造。

正确解:传引用调用

    Date(const Date& d)//加const修饰的原因是防止因为代码写反位置而导致拷贝失败的情况发生。如line3
    {
      	//d._year = _year;
        _year = d._year;
        _month = d._month;
        _date = d._date;
    }

拷贝构造函数也是特殊的成员函数,其特征如下:

  1. 拷贝构造函数是构造函数的一个重载形式。

  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。

  3. 若未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数:

    内置类型成员对象会按照存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

    自定义类型成员对象会调用它的拷贝构造,如果没有同样会进行浅拷贝,如果成员对象有指针,就会导致他们指向的空间被析构两次,导致程序崩溃。

  4. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,像日期类这样的类是没必要自己实现拷贝构造函数的。但是对于自定义类型,必须自己实现深拷贝

  5. 类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

  6. 拷贝构造函数典型调用场景:

    • 使用已存在对象创建新对象
    • 函数参数类型为类类型对象
    • 函数返回值类型为类类型对象

为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。

写了拷贝构造,就必须得写构造函数,因为写的拷贝构造也算是一种构造函数,系统不会默认生成构造函数了。

一道题目:数拷贝构造的次数

Widget f(Widget u)
{
    Widget v(u);
    Widget w = v;
    return w;
}
int main()
{
    Widget x;
    Widget y = f(f(x));
    return 0;
}

看到题目我们必须得先搞清楚拷贝构造的编译器优化问题:传参或传返回的过程中,存在连续构造、拷贝构造,就会被优化

//创建类
class Widget
{
public:
    Widget()
    {
        cout << "Widget()" << endl;
    }
    Widget(const Widget& )
    {
        cout << "Widget(const Widget &)" << endl;
    }
    Widget operator=(const Widget &)
    {
        cout << "Widget operator=(const Widget &)" << endl;
        return *this;
    }
    ~Widget()
    {
        //cout << "~Widget()" << endl;
    }
};
//首先创建一个简单函数
Widget f(Widget u)//传值传参拷贝一次
{
    return u;//传值返回拷贝一次
}
int main()
{
    // Widget x;
 		//并非匿名函数,生命周期只在当前函数
    //Widget y = f(x);//传值给y一次
    //总共应该三次拷贝构造。
    //但是,一次调用里面连续构造函数会被编译器优化,合二为一。
  
    //但是如果这样就不会被优化。
    //Widget y;
    //y = f(x);
  	//原因:构造和拷贝构造被分开了。
  
    //Widget();//匿名对象生命周期只在这一行
    //匿名对象
		//f(Widget());//只是拷贝构造一次。被优化,两次拷贝构造合二为一了
    // Widget x;
    // f(x);//拷贝构造两次 传值拷贝--传值返回
    return 0;
}

所以需要搞清楚,连续的构造和拷贝构造时如果出现了a->b->c,而b根本不重要的时候,编译器会优化拷贝构造,成一次。

Widget f(Widget u)//传值传参
{
    Widget v(u);//拷贝构造
    Widget w = v;//拷贝构造
    return w;//传值返回
}//调用完整f(x)的是四次
int main()
{
    Widget x;
    Widget y = f(f(x));//本来是4*2+1次,
    //但是f(x)的返回值和f( f(x) )的传参的连续的两次拷贝构造合并了。
    //f(f(x))的传值返回和y的拷贝构造 这两次又合并了。
    return 0;
}

正解7次。

赋值重载

学习赋值重载,我们要先学习运算符重载,学完运算符重载之后,赋值重载只是这里面的一种情况。所以推荐先看下一个章节(就在下文)的内容,随后返回来看赋值重载。

赋值运算符重载格式:

  • 参数类型:const Date&,传引用可以提高传参效率
  • 返回值类型:Date&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值。
  • 检测是否自己给自己赋值
  • 返回*this:要复合连续赋值的含义
    //返回类型为void的赋值重载:由于返回值是void,所以只能赋值一次,不能像int一样可以连续赋值,如x=y=z
    // void operator=(const Date& d)
    // {
    //     _year = d._year;
    //     _month = d._month;
    //     _date = d._date;
    // }
    //连续赋值重载
    Date& operator=(const Date& d)
    {
            _year = d._year;
            _month = d._month;
            _date = d._date;
            //并不是返回this,因为this是个指针
            //返回*this
            //直接传值返回会进行一次拷贝构造
            //由于*this是全局的,出作用域不会销毁。
            //我们可以传引用返回。
            return *this;
    }

    //自己给自己赋值的优化
    Date &operator=(const Date &d)
    {
      	//自己给自己赋值就不用处理了,可以跳过。
        if(this!=&d)
        {
            _year = d._year;
            _month = d._month;
            _date = d._date;
        }

        return *this;
    }
int main()
{

    Date d1(2022, 10, 16);
    Date d3(2000, 12, 05);
    //一个已经存在的对象初始化一个马上创建实例化的对象
    Date d2(d1); //拷贝构造

    //两个已经存在的对象之间进行赋值拷贝
    d3 = d1;//赋值重载
    d3.operator=(d1);
    return 0;
}

赋值运算符只能重载成类的成员函数不能重载成全局函数

原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。

编译器默认生成的赋值重载,跟拷贝构造做的事情完全类似:

  1. 内置类型成员变量,会完成字节序值的拷贝 – 浅拷贝
  2. 自定义类型成员变量,会调用它的operator=

与拷贝构造相同,类中如果没有涉及资源申请时,赋值重载函数是否写都可以;一旦涉及到资源申请时,则赋值重载函数是一定要写的,否则就是浅拷贝。

区分拷贝构造和赋值重载:

Date d1(2022,10,16);
Date d2 = d1;

这是拷贝构造!!二者本质的区别在于:

  • 一个已经存在的对象初始化一个马上创建实例化的对象是拷贝构造
  • 两个已经存在的对象之间进行赋值拷贝

取地址及const取地址运算符重载

这两个默认成员函数一般不用重新定义,编译器默认会生成。

要写非常简单:

Date* Date::operator&()
{
    return this;
}
const Date* Date::operator&() const
{
    return this;
}

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!

运算符重载

默认情况下C++是不支持自定义类型对象使用运算符的。

**C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,**也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名字为:关键字operator后面接需要重载的运算符符号
函数原型:返回值类型 operator操作符(参数列表)

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  • (.*) (:😃 (sizeof) (?😃 (.) 注意以上5个运算符不能重载。这个经常在笔试选择题中出
    现。

成员变量私有导致的访问问题

我们还是以Date类来进行说明,写一个日期的大于运算符

//函数名就是operator操作符
//返回类型要看操作符运算后返回的值是什么
//操作符有几个操作时就有几个参数
bool operator>(const Date &d1, const Date &d2)
{
    if (d1._year > d2._year)
    {
        return true;
    }
    else if (d1._year == d2._year && d1.month > d2._month)
    {
        return true;
    }
    else if (d1._year == d2._year && d1.month == d2._month && d1.date > d2.date)
    {
        return true;
    }
    return false;
}

但是这面临着一个问题:成员变量的私有导致的无法访问。

解决它有以下几种方法:

  • 成员变量公有化,变private为public。
  • 类内 设定GetYear,GetMonth, GetDate函数,调用函数解决。
  • 我们把这个操作符重载函数拿到类内。

我们使用法3,

class Date
{
public:
    Date(int year = 0, int month = 1, int date = 1)
    {
        _year = year;
        _month = month;
        _date = date;
    }
    ~Date()
    {
    } // Date类没有资源需要清理,所以Date不实现析构函数都是可以的。
    Date(Date &d)
    {
        _year = d._year;
        _month = d._month;
        _date = d._date;
    }
    bool operator>(const Date &d1, const Date &d2)
    {
        if (d1._year > d2._year)
        {
            return true;
        }
        else if (d1._year == d2._year && d1.month > d2._month)
        {
            return true;
        }
        else if (d1._year == d2._year && d1.month == d2._month && d1.date > d2.date)
        {
            return true;
        }
        return false;
    }

private:
    int _year;
    int _month;
    int _date;
};
CleanShot 2022-10-16 at 14.48.59@2x

会发现报错显示参数太多。这是因为隐藏的this指针就算作了一个参数,从而导致实际上有三个参数。这时候d1和this是重复的,我们可以略去d1参数。如下写:

class Date
{
public:
    Date(int year = 0, int month = 1, int date = 1)
    {
        _year = year;
        _month = month;
        _date = date;
    }
    ~Date()
    {
    } // Date类没有资源需要清理,所以Date不实现析构函数都是可以的。
    Date(Date &d)
    {
        _year = d._year;
        _month = d._month;
        _date = d._date;
    }
    bool operator>(const Date &d)
    {
        if (_year > d._year)
        {
            return true;
        }
        else if (_year == d._year && _month > d._month)
        {
            return true;
        }
        else if (_year == d._year && _month == d._month && date > d._date)
        {
            return true;
        }
        return false;
    }

private:
    int _year;
    int _month;
    int _date;
};

重载运算符的调用

  1. 在类外定义的运算符:
int main()
{
    Date d1(2022, 10, 16);
    Date d2(2022,12,16);
  	//调用
    cout << (d1 > d2) << endl;//必须加括号!流插入操作符优先级更高
    cout << operator>(d1, d2) << endl;
    return 0;
}

调用运算符的两种方式,一种是直接使用运算符,一种是调用函数使用操作符,如上line6, line7。

  1. 在类内定义的运算符:
 (d1 > d2);
 d1.operator>(d2);

一般用line1。

  1. 如果全局和成员中都进行定义,定义了相同的运算符重载函数,会优先在成员中查找。但是正常情况下是写在成员里(本来写在成员里就是规避在类外写而成员变量私有的问题。)

const成员

将const修饰的”成员函数"称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

void TestDate5()
{
    //我们说了成员函数调用的时候会默认产生this指针,这个this指针的类型默认是Date* const this类型。其中const是用来保护this不被改变。

    Date d1;    //取地址后是Date*,可以传过去
    d1.Print(); //传过去的this指针是Date* const this类型,权限不变

    // const Date d2;//取地址后类型为 const Date*
    // d2.Print();   //传过去的this指针依然为Date* const this,会导致权限放大。
    //我们应该在定义函数后面加const,如下
    // void Date::Print() const

    //这样,或权限缩小,或权限不变,都可以运行通过。
}

总结:成员函数加const是好的,建议能加const都加上。这样普通对象和const对象都可以调用了。但是如果要修改成员变量的函数不能加const。

深入理解构造函数

构造函数体赋值

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。

class Date
{
public:
Date (int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month:
_day = day;
}
private:
int _year;
int _month;
int _day;
};

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体內可以多次赋值

初始化列表

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。

    Date(int year = 0, int month = 1, int day = 1)
        :_year(year)
        ,_month(month)
        ,_day(day)
    {
    }

【注意】

  1. 我们需要分清楚以下几个问题:

    • //成员变量的声明:
      //在定义类时
      private:
      		int _year;
      		int _month;
      		int _day;
      
    • //对象的定义/对象的实例化
      //在main函数中
      int main()
      {
      Date d1(2022,10,17);//对象定义
      return 0;
      }
      
    • 成员变量什么时候初始化呢?

      比如我们想定义

      int i;//可以
      //const int j;//不可以!常量必须在定义时初始化,不初始化就报错
      const int j = 0;
      

      如果在声明/定义的时候,不完成初始化,对于const类型变量是行不通的。

      所以C++对此引入初始化列表,这个初始化列表就被认为是成员变量定义的地方,并且可以在定义的时候初始化。

    • 我们可以选择在

      构造函数体内初始化 (定义时先不初始化,类似int i; i = 10; )

      也可以选择在初始化列表初始化 (定义时就完成初始化, 类似int i = 10; )

      这个我们可以灵活控制。

  2. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

  3. 类中包含以下成员**,必须放在初始化列表位置进行初始化:**

    • 引用成员变量 (因为引用必须在定义的时候初始化)

    • const成员变量

    • 自定义类型成员(且该类没有默认构造函数时)

      默认构造函数是 不用传参的构造函数。因为自定义类型成员初始化必须在定义的地方,初始化列表是其定义的地方,在这就会调用默认构造函数,如果没有默认构造函数,就必须在这里手动初始化,否则会报错。

  4. 自定义类型成员变量建议在初始化列表初始化。如果坚持在构造函数体内初始化,效率低。

    因为我们如果把自定义类型的初始化放在函数体内,会进行多次构造和赋值。因为,虽然我们没有写初始化列表,但是初始化列表是成员变量定义的地方,自定义类型成员变量会在初始化列表调用默认构造函数进行初始化。

  5. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。

    看题:

    class A
    {
    public:
    A(int a	
    		:_al(a)
    		,_a2 (_a1)
    {}
    
    void Print() {
    Cout<<_al<<" "<<_a2<<endl;}
    private:
    int _a2;
    int _al;
    };
    
    int main() {
    A aa(1);
    aa. Print();
    }
    
      //问:
      //A:输出1 1
      //B:崩溃
      //C:编译不通过
      //D:输出1 随机值
      
      //答:
      //选D
      //因为声明次序就是初始化顺序。
    

    建议:一个类,尽量声明的顺序就是初始化的顺序,保持统一。

总结:

  1. 初始化列表 - 成员变量定义的地方
  2. const、引用、没有默认构造函数的自定义类型成员变量必须在初始化列表初始化。(因为他们都必须在定义的地方初始化)
  3. 对于其他类型的成员变量如int等,在哪初始化都可以。
  4. 建议在初始化列表初始化。如果坚持在构造函数体内初始化,效率低。
  5. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

explicit关键字

如果我们简单定义个类

class Year
{
public:
		Year(int year)
		:_year(year)
		{}
private:
		int _year;
};

c++是默认支持隐式类型转换的,这就代表着:

构造函数不仅可以构造初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。

//不但可以这样:
Year year(2022);
//也可以这样:
Year year = 2022;
//line4同样调用的是构造函数

借用标准里的话来说,就是当你只有一个类型T1,但是当前表达式需要类型为T2的值,如果这时候T1自动转换为了T2那么这就是隐式类型转换。比如

//相近类型 - 表示意义相似的类型
double d = 1.1;
int i = d;
//而且d并非直接转给i
//而是生成一个中间变量, 这个中间变量是临时变量,具有常性,也就是不能被修改
//所以我们要引用的话必须加const,否则权限放大,报错。
const int& i = d;

但是为什么整型的year可以隐式类型转换为Year类型呢?

可以认为创建了临时对象,然后再用这个对象拷贝构造year。如下

Year tmp(2022);//一次构造
Year year = tmp;//一次拷贝构造

但是c++编译器在连续的一个过程中,多个构造会被优化,合二为一,所以在这里被优化为只有一个构造。所以,

Year year(2022);
Year year = 2022;
//

虽然他们的结果都是直接构造一次,但是过程不一样的。如果我们不想让这个隐式类型转换发生,我们可以加关键字explicit在构造函数之前。如下:

	explicit Year(int year)
		:_year(year)
		{}
//explicit修饰构造函数,禁止类型转换

总结:用explicit修饰构造函数,将会禁止构造函数的隐式转换。

static成员

下面看引入,我们想看一个类,计算程序中创建了多少个对象。如果我们把count写成全局变量类似这样:

#include <iostream>
// using namespace std;
using std::cout;
using std::endl;
//不能全部释放std因为std定义了count。

int count = 0;
class A
{
public:
    A(int a = 0)
        : _a(a)
    {
        count++;
    }
    A(const A &aa)
        : _a(aa._a)
    {
        count++;
    }

private:
    int _a;
};
void f(A a)
{}
int main()
{
    A a1;
    A a2 = 1;
    f(a1);
		cout << count << endl;

    return 0;
}

这样会造成一个问题:

  • count不被保护,我们可以在任何地方调用count,也可以在任何地方修改count,一旦写错程序,把count的值修改,就会导致计数错误。

能不能让count和类绑定呢?可以,只需要把count写在成员变量里。但是这样就会造成,如果我们有超过一个类,不同的类都各自有一个count,而每个count都各自独立。我们希望的是count可以计数不同的类的对象创建个数,所以不可取。

C++在此引入了在类中定义静态成员,static

private:
    static int _scount; // 类内声明
    int _a;
};
//静态成员变量属于整个类,所有对象,生命周期在整个程序运行期间。
int A::_scount = 0;//类外定义

静态成员变量属于整个类,所有对象,生命周期在整个程序运行期间。在类成员函数中可以随便访问

但是因为定义在类里,为私有变量,所以无法访问。访问的方式有如下几种:

  • 设定public成员函数获取

           int GetSCount()
        {
            return _scount;
        }
       
       //cout << A::GetSCount() << endl;
        //非静态成员引用必须与特定对象相对,所以不能使用类调用
        cout << a1.GetSCount() << endl;
        cout << a2.GetSCount() << endl;
    

    但是这并非最好的方式,因为不能类调用。

  • 静态成员函数

    只需要在原来的public成员函数基础上前置static,就将成员函数设定为静态成员函数。

         static int GetSCount()
        {
            return _scount;
        }
       
        cout << A::GetSCount() << endl;
        cout << a1.GetSCount() << endl;
        cout << a2.GetSCount() << endl;
    
    

总结:声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。

  1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
  2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
  3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  5. 静态成员也是类的成员,受public、 protected、 private 访问限定符的限制

题目:求1+2+3+4+…+n,不能使用乘除法、for、while、if、else、switch、case、sizeof等关键字及条件判断语句。不能递归、不能位运算、不能公式直接计算。

要求空间复杂度O(1), 时间复杂度O (N)。

解答:解题的基本思路其实就是循环、递归、公式计算,但都被限制了。

class Sum
{
    public:
    Sum()
    {
        _ret += _i;
        ++_i;
    }
    static int GetRet()
    {
        return _ret;
    }

    private:
        static int _i;
        static int _ret;
};
int Sum::_i = 1;
int Sum::_ret = 0;
class solution
{
private:
    /* data */
public:
    int Sum_Solution(int n){
        //Sum a[n];//循环n次
        //return Sum().GetRet() - (n + 1);//用匿名对象调用的,多调用了一次,所以需要-(n+1)
    //或者这样        
        //Sum a[n - 1];
        //return Sum().GetRet();
    //或者不用对象去调用,更优,需要静态函数
        Sum a[n];
        return Sum::GetRet();
    }
};
int main()
{
    int n;
    cin >> n;
    cout << solution().Sum_Solution(n) << endl;
    return 0;
}

C++11 中提供成员变量缺省值

由于在C++中,默认生成的构造函数只会初始化自定义类型而忽略了系内置类型,所以需要另外对内置类型的成员变量进行初始化,非常麻烦。在C++11中引入成员变量声明时可以带缺省值,类似下:

class Date
{
public:
    // Date(int year = 0, int month = 1, int day = 1)
    // {
    //     _year = year;
    //     _month = month;
    //     _day = day;
    // }
    //初始化列表
    Date(int year = 0, int month = 1, int day = 1)
        : _year(year), _month(month), _day(day)
    {
    }

private:
    int _year = 0;
    int _month = 1;
    int _day = 1;
};

由于这只是声明,所以并非初始化,是给成员变量缺省值。

如果在初始化列表阶段没有对成员变量初始化,编译器就会使用缺省值初始化。

而且不但可以给内置类型缺省值,还可以给自定义类型成员变量缺省值。

静态成员变量不能这样给缺省值,必须在类外全局位置定义初始化(给缺省值是为了简化初始化列表阶段,但是静态成员变量不由初始化列表初始化)。

友元

友元的引入

我们一般输出Date类的对象时都采用如下调用成员函数的方法:

d1.Print();

但是如果我们就是想使用流插入方式输出,我们就必须重载operator<<。


错误示范:

void Date::operator<<(ostream &out)//cout的类型是ostream
{
    out << _year << "-" << _month << "-" << _date << endl;
}

我们可以发现如果我们调用是失败的。

cout<<d1;//报错
d1.operator(cout);//成功
d1<<cout;//成功

这是因为,运算符重载里面,如果是双操作数的操作符重载,第一个参数是操作数,第二个参数是右操作数。因为cout是<<的左操作数,而d1是其右操作数,this会默认指向cout的地址。


所以可以发现,其实是因为在类内定义成员函数中的this导致出错的,那么我们可以不写成员函数,而把他写成全局函数。

void operator<<(ostream &out,const Date& d)//cout的类型是ostream
{
    out << d._year << "-" << d._month << "-" << d._date << endl;
}

但是!!!这样就不能访问私有成员函数了。即便我们写成员函数GetMonth, GetYear等来获取成员变量的值,进行流插入。也无法进行流提取,因为流提取需要改变成员变量的值,这是private成员变量绝对无法做到的事情。

这时,C++选择采用友元函数来解决这个问题。

流插入运算符重载

在类内函数声明前置friend 来进行声明,这是友元函数。

//类内声明
friend void operator<<(ostream &out,const Date d);
//类外定义
void operator<<(ostream &out, const Date d)
{
    out << d._year << "-" << d._month << "-" << d._date << endl;
}

这样可以运行了,但是仍然不能连续的输出,类似cout<<d1<<d2<<endl; 这是因为我们定义的重载函数没有进行返回。

ostream &operator<<(ostream &out, const Date d)
{
    out << d._year << "-" << d._month << "-" << d._date << endl;
    return out;
}
流提取运算符重载
//类内声明
friend istream &operator>>(istream &in, Date& d);
//类外定义
istream &operator>>(istream &in, Date &d)
{
    in>>d._year>>d._month>>d._date;
    return in;
}

简介

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封裝,也破坏了管理规则。所以友元不宜多用。能不用就不用。

友元分为:友元函数和友元类

友元函数

问题:现在尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成
全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。

友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字

说明:

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

友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

  • 友元关系是单向的,不具有交换性。

  • 比如Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。

  • 友元关系不能传递

    如果C是B的友元,B是A的友元,则不能说明C是A的友元。

  • 友元关系不能继承,在继承位置再给大家详细介绍。

内部类

概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类

内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。内部类和在全局定义是基本一样的,只是他受到外部类的类域限制。

注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。

特性:

  1. 内部类可以定义在外部类的public、 protected、 private,都是可以的。
  2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  3. sizeof(外部类)=外部类,和内部类没有任何关系。
  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值