> 技术文档 > 【C++】类和对象(下)

【C++】类和对象(下)


一. 再谈构造函数

构造函数里,类初始化成对象有2种方式:构造函数体赋值、初始化列表

之前学的是构造函数体赋值:

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

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

注意:
        1. 每个成员变量在初始化列表中最多只能出现一次(初始化只能初始化一次)
        2. 类中包含以下成员,必须放在初始化列表位置进行初始化:引用、const、自定义类型成员(且该类没有默认构造函数时)

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

为什么设计初始化列表?

引用、const 成员必须在定义的时候初始化:
        引用:必须变成谁的别名
        const:只有1次初始化的机会(定义的时候)

什么是定义的地方?

对象实例化时整体定义。调用构造函数,对每个成员初始化
对象里有多个成员变量,每个成员变量在初始化列表定义

class A{public:A(int a = 0):_a(a){cout << \"A(int a = 0)\" << endl;}private:int _a;};class C{public:C(int c):_c(c){cout << \"C(int c)\" << endl;}private:int _c;};class B{public:// B(int a, int ref) 引用的局部变量,出作用域销毁 B(int a, int& ref) // ref是n的别名:_ref(ref) // _ref是ref(n)的别名,_n(1),_z(9) // 显示给,不用缺省值,_ck(10) // 没有默认构造,必须在这里显示调用构造,_ak(99) // 有默认构造也可以显示调(缺省参数){}private:A _aobj; // 有默认构造函数A _ak;C _ck; // 没有默认构造函数int& _ref; // 引用const int _n; // const int _x;int _y = 1; // 1是缺省值,是给初始化列表的int _z = 1;};int main(){// B bb(10, 1);int n = 1; B bb(10, n); return 0;}

我们不写,内置类型不处理;自定义类型调用(在初始化列表调用)默认构造函数

typedef int DateType;class Stack{public:Stack(int capacity = 4) // 构造函数,功能:替代Init{_a = (DateType*)malloc(sizeof(DateType) * capacity);if (nullptr == _a){perror(\"malloc申请空间失败!!!\");return;}_capacity = capacity;_size = 0;}void Push(DateType Date){CheckCapacity();_a[_size] = Date;_size++;}void Pop(){if (Empty())return;_size--;}DateType Top() { return _a[_size - 1]; }int Empty() { return 0 == _size; }int Size() { return _size; }~Stack(){cout << \"~Stack()\" << endl;if (_a){free(_a);_a = nullptr;_capacity = 0;_size = 0;}}private:void CheckCapacity(){if (_size == _capacity){int newcapacity = _capacity * 2;DateType* temp = (DateType*)realloc(_a, newcapacity *sizeof(DateType));if (temp == nullptr){perror(\"realloc申请空间失败!!!\");return;}_a = temp;_capacity = newcapacity;}}private:DateType* _a;int _capacity;int _size;};class MyQueue{public:MyQueue() // 不传参就这样写{ }MyQueue(int capacity) // 自己控制capacity,手动传参:_pushst(capacity),_popst(capacity){ }private:Stack _pushst;Stack _popst;};int main(){MyQueue q1; // 调用无参的构造函数MyQueue q2(100); // 调用带参的构造函数(构造函数可以构成重载)return 0;}

都会对 _pushst _popst 初始化,所有成员都会走且只能走1次初始化列表因为那是它定义的地方

写了就用你的,不写也会走初始化列表

自定义类型必须调用构造函数,引用、const 必须在定义的时候初始化
        有默认构造函数:我就调默认构造函数,不传参也可以
        没有默认构造函数(eg:只有一个带参的构造函数):在初始化列表时就不知道怎么初始化成员了

所以:没有默认构造函数,编译器不知道怎么初始化成员,就报错了,他要求你来


初始化列表并不能解决所有问题

eg1:要求 检查、初始化数组

class Stack{public:Stack(int capacity = 10): _a((int*)malloc(capacity * sizeof(int))),_top(0),_capacity(capacity){if (nullptr == _a){perror(\"malloc申请空间失败\");exit(-1);}// 要求数组初始化一下memset(_a, 0, sizeof(int) * capacity);}private:int* _a;int _top;int _capacity;};

eg2:动态开辟二维数组

class AA{public:AA(int row = 10, int col = 5):_row(row),_col(col){_aa = (int**)malloc(sizeof(int*) * row);for (int i = 0; i < row; i++){_aa[i] = (int*)malloc(sizeof(int) * col);}}private:int** _aa;int _row;int _col;};

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

声明、定义的顺序要保持一致

class A{public:A(int a):_a1(a),_a2(_a1) // 先走这个{}void Print() {cout << _a1 << \" \" << _a2 << endl; // 1 随机值}private:int _a2;int _a1;};int main() {A aa(1);aa.Print();}

explicit关键字

类型转换中间会产生临时变量

int i = 10;double d = i;

中间会产生 double 类型的临时变量,临时变量再给 d

临时变量又有常性,所以 double& d = i; 错误         const double& d = i; 正确

class A{public:A(int a):_a(a){ }private:int _a;};int main(){A aa1(1); // 调构造A aa2 = 2; // 隐式类型转换,整型转化为自定义类型return 0;}

用 2 调用构造函数,生成 A 类型的临时对象;临时对象再拷贝构造 aa2(构造+拷贝构造)

在同一个表达式内,连续构造会被优化:用 2 直接构造


验证:用 2 直接构造

class A{public:A(int a) // 构造函数:_a(a){cout << \"A(int a)\" << endl;}A(const A& aa) // 拷贝构造函数:_a(aa._a){cout << \"A(const A& aa)\" << endl;}private:int _a;};int main(){A aa2 = 2; // 隐式类型转换,整型转化为自定义类型return 0;}

验证:用 2 调用构造函数,生成 A 类型的临时对象;临时对象再拷贝构造 aa2(构造+拷贝构造)

int main(){A& aa3 = 2; // aa3 引用 aa2 肯定没毛病,但不能引用 2 // error C2440: “初始化”: 无法从“int”转换为“A &” const A& aa3 = 2; // return 0;}

引用不可以;const引用 可以,还调了一次构造:


这个玩法的用处:

#include //#include //class string//{//public://string(const char* str) // string类中的一个构造函数,可以用一个字符串去构造string类//{}//};class list{public:void push_back(const string& str){}};int main(){string name1(\"张三\"); // 构造string name2 = \"张三\"; // 构造+拷贝构造+优化 // 结果一样,但过程不一样 list lt1;string name3(\"李四\");lt1.push_back(name3); // 老老实实构造,你是string,我传string给你lt1.push_back(\"李四\"); // 更舒服return 0;}

支持 28行写法的原因:隐式类型转换

\"李四\"是 const char*,会去调它的构造(string(const char* str)),构造一个临时对象;
临时对象有常性,符合void push_back(const string& str)


如果不想让转换发生呢?

class A{public:explicit A(int a) // 构造函数:_a(a){cout << \"A(int a)\" << endl;}A(const A& aa) // 拷贝构造函数:_a(aa._a){cout << \"A(const A& aa)\" << endl;}private:int _a;};int main(){A aa2 = 2; // error C2440: “初始化”: 无法从“int”转换为“A”const A& aa3 = 2; // error C2440: “初始化”: 无法从“int”转换为“const A &”return 0;}

智能指针就不想发生转换

二. static 成员

想统计 A 创建了多少个对象

先想到定义全局变量

int _scount = 0;class A{public:A() { ++_scount; }A(const A& t) { ++_scount; }~A() { --_scount; }// static int GetACount() { return _scount; }private:// static int _scount;};A aa0;A Func(A aa1){cout << __LINE__ << \":\" << _scount << endl;return aa;}int main(){cout << __LINE__ << \":\" << _scount << endl;A aa1;static A aa2;Func(aa1);cout << __LINE__ << \":\" << _scount << endl;return 0;}

输出结果:24:1         18:4         28:3

24行的那一个对象是 aa0
说明:全局对象在 main 函数之前就会调用构造;局部的静态对象不会在 main 函数之前初始化

到 18 行,有 aa0 aa1 aa2,还有一个是自定义类型传参调用的拷贝构造

传值返回,27行结束就销毁了

A aa0;void Func(){static A aa2;cout << __LINE__ << \":\" << _scount << endl;}int main(){cout << __LINE__ << \":\" << _scount << endl; // 1A aa1;Func(); // 3Func(); // 3return 0;}

aa2 是局部的静态对象,不在函数栈帧里,在静态区,只会定义 1 次

祖师爷不喜欢这种方式,不想让你随便访问,提出封装的方式


2.1 概念

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

2.2 特性

  1. 静态成员所有类对象所共享,不属于某个具体的对象,存放在静态区
  2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
  3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制
  6. 静态成员变量不能给缺省值。缺省值是给初始化列表用的,它没有初始化列表
class A{public:A() { ++_scount; }A(const A& t) { ++_scount; }~A() { --_scount; } // 3.没有this指针,指定类域和访问限定符就可以访问static int GetACount() { // _a1++; 错! 5.静态里,不能访问非静态的,因为没有this指针 return _scount; }private:// 成员变量 -- 属于每个一个类对象,存储对象里面int _a1 = 1;int _a2 = 2;// 静态成员变量 -- 1.属于类,属于类的每个对象共享,存储在静态区(生命周期是全局的)static int _scount;};// 2.全局位置,类外面定义。不能在初始化列表定义,因为它不是对象自己的成员int A::_scount = 0;A aa0;void Func(){static A aa2;cout << __LINE__ << \":\" << aa2.GetACount() << endl; // 3.对象.静态成员}int main(){cout <<__LINE__<<\":\"<< A::GetACount() << endl; // 3.类名::静态成员A aa1;Func();Func();return 0;}

因为是私有,上面就不能直接访问 _scount,只能通过公有的成员函数

静态成员变量和静态成员函数一般都是配套出现


问题:
        1. 静态成员函数 可以调用 非静态成员函数 吗?
        2. 非静态成员函数 可以调用 类的静态成员函数 吗?

class A{public:A() { ++_scount; }A(const A& t) { ++_scount; }~A() { --_scount; }void Func1(){// 非静态能否调用静态:可以:没有类域、限定符限制GetACount();}void Func2(){++_a1;}static int GetACount(){// 静态能否调用非静态:不可以:非静态的成员函数调用需要this指针,我没有thisFunc2();// _a1++; 静态里,不能访问非静态的,因为没有this指针return _scount; }private:// 成员变量int _a1 = 1;int _a2 = 2;// 静态成员变量static int _scount;};

一个用 static 的绝佳场景(一种思想):

设计一个类,在类外面只能在栈上创建对象
设计一个类,在类外面只能在堆上创建对象

class A{public:static A GetStackObj(){A aa;return aa;}static A* GetHeapObj(){return new A;}private:A(){}private:int _a1 = 1;int _a2 = 2;};int main(){//static A aa1; // 静态区//A aa2; // 栈 //A* ptr = new A; // 堆A::GetStackObj();A::GetHeapObj();return 0;}

调用这个成员函数需要对象,但这个函数是为了获取对象(先有鸡还是蛋的问题)用 static 可以解决

三. 友元

友元提供了一种 突破封装 的方式,提供了便利。但会增加耦合度,破坏了封装,所以友元不宜多用

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

1. 友元函数

我的函数声明成你的朋友,我在类外面就可以访问你的私有

说明:

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

在类外面有时需要用对象访问成员。

以前访问的方式是成员函数,成员函数的第一个位置都是this指针

下面的代码要符合用法,流对象 ostream的cout对象 和 istream的cin对象 要抢占左操作数。写成成员函数会抢位置,所以不能写成成员函数(详解见上一篇文章的 流插入打印、流提取 )【C++】类和对象(中)拷贝构造、赋值重载_c++构造值一样的对象-CSDN博客

        Date.h

class Date{// 友元函数声明friend ostream& operator<>(istream& in, Date& d);public:Date(int year = 1900, int month = 1, int day = 1); // 构造 void Print() const{cout << _year << \"-\" << _month << \"-\" << _day << endl;}private:int _year;int _month;int _day;};ostream& operator<>(istream& in, Date& d);

        Date.cpp

Date::Date(int year, int month, int day) // 构造{if (month > 0 && month  0 && day <= GetMonthDay(year, month)){_year = year;_month = month;_day = day;}else{cout << \"非法日期\" << endl;assert(false);}}ostream& operator<<(ostream& out, const Date& d){out << d._year << \"年\" << d._month << \"月\" << d._day << \"日\" <>(istream& in, Date& d){int year, month, day;in >> year >> month >> day;if (month > 0 && month  0 && day <= d.GetMonthDay(year, month)){d._year = year;d._month = month;d._day = day;}else{cout << \"非法日期\" << endl;assert(false);}return in;}

友元提供了便利,增加了耦合(关联度)

eg:不想叫_year,想叫year,类里面得改,友元函数里也得改

    2. 友元类

    我的类成为你的友元,在我整个类里面,可以随便访问你的私有、保护

    说明:

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

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

    • 友元关系不能传递

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

    • 友元关系不能继承,在继承位置再给大家详细介绍
    class Time{friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量public:Time(int hour = 0, int minute = 0, int second = 0): _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;};

    四. 内部类

    概念:如果一个类定义在另一个类的内部,这个内部的类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。

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

    特性:

    1. 内部类可以定义在外部类的public、protected、private都是可以的。
    2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
    3. sizeof(外部类)=外部类,和内部类没有任何关系。
    4. 受访问限定符的限制

    定义出来给别人用的东西(目前学的:成员变量,成员函数,内部类)才会收到访问限定符的限制


    class A{private: static int k; int h;public: class B { public: void foo() { } private: int b; };};int A::k = 1;int main(){ cout << sizeof(A) << endl; // 4 A aa; A::B bb; // 如果 B是私有的,这样就错return 0;}

    k 没有存在对象里,所以不计算 k 的大小
    A类 里面没有创建 B对象,所以不算 b 的大小

    class A{private: static int k; int h;public: class B { public: void foo() { } private: int b; }; B _bb; // A类 里,用B类 创建了对象 _bb。要算 _bb的大小};int A::k = 1;int main(){ cout << sizeof(A) << endl; // 8return 0;}

    class A{private:static int k;int h;public:class B // B天生就是A的友元,内部类是外部类的天生友元{public:void foo(const A& a){cout << k << endl; // OKcout << a.h << endl; // OK}};};int A::k = 1;int main(){A::B b;b.foo(A());return 0;}

    五. 匿名对象

    class A{public: A(int a = 0) :_a(a) { cout << \"A(int a)\" << endl; } ~A() { cout << \"~A()\" << endl; }private: int _a;};class Solution{public: int Sum_Solution(int n) { cout << \"Sum_Solution\" << endl; //... return n; }};
    int main(){ A aa(1); // 有名对象 -- 生命周期在当前函数局部域 A(2); // 匿名对象 -- 生命周期在当前行 // 后面没人用,干脆直接销毁// 想调用Sum_Solution函数// 1.有名对象 Solution sl; // 不能加():Solution sl(); // 不知道是对象还是函数名 sl.Sum_Solution(10);// 2.匿名对象 Solution().Sum_Solution(20); // 必须加() // Solution.Sum_Solution(20); // 错,必须是 \"对象.\" 要传this // Solution::Sum_Solution(20); // 错,只有静态成员函数才能这么调,因为没有this指针 // A& ra = A(1); // 错,匿名对象具有常性 const A& ra = A(1); // const引用延长匿名对象的生命周期,生命周期在当前函数局部域 // ra还要用,留下来 Solution().Sum_Solution(20); return 0;}


    void push_back(const string& s) // 如果不加const,仅第一个可以编过{ cout << \"push_back:\" << s << endl;}int main(){ string str(\"11111\"); // 有名对象 push_back(str); push_back(string(\"222222\")); // 匿名对象。传上去const引用,延长生命周期 push_back(\"222222\"); // 临时对象。隐式类型转换(详解见上文explicit关键字) return 0;}

    六. 拷贝对象时的一些编译器优化

    class A{public: A(int a = 0) :_a(a) { cout << \"A(int a)\" << endl; } A(const A& aa) :_a(aa._a) { cout << \"A(const A& aa)\" << endl; } A& operator=(const A& aa) { cout << \"A& operator=(const A& aa)\" << endl; if (this != &aa) { _a = aa._a; } return *this; } ~A() { cout << \"~A()\" << endl; }private: int _a;};
    void Func1(A aa){}void Func2(const A& aa){}int main(){ A a1; Func1(a1); // 传值传参:a1传给aa,要调用拷贝构造 Func2(a1); // 没有拷贝构造 return 0;}

    void Func1(A aa){}void Func1(const A& aa) // 类型不一样,构成重载{}int main(){ A a1; Func1(a1); // “Func1”: 对重载函数的调用不明确  无参、全缺省 return 0;}

    A Func3(){ A aa; return aa;}int main(){ Func3(); return 0;}

    旧编译器:传值返回,返回aa的拷贝        

    新编译器:连续的构造+拷贝 ==> 直接构造        

    A& Func4() // 只有静态,出了作用域没有销毁(*this)才能用引用返回{ static A aa; return aa;}int main(){ Func4(); return 0;}

    传引用返回,没有拷贝


    A Func5(){ A aa; return aa;}int main(){ Func5(); // 这个表达式返回的值是aa拷贝的临时对象 // 所以不能这样接收:A& ra = Func5(); const A& ra = Func5(); return 0;}
    A Func5(){ A aa; return aa;}int main(){ A ra = Func5(); return 0;}

    同一行连续的一个步骤里,>=2个 构造\\拷贝构造\\构造+拷贝构造 有可能优化

    void Func1(A aa){}A Func5(){ A aa; return aa;}int main(){ A ra = Func5(); // 拷贝构造+拷贝构造 --> 优化为拷贝构造 A aa1; Func1(aa1); // 不优化。在2行 Func1(A(1)); // 构造+拷贝构造 --> 优化为构造 Func1(1); // 构造+拷贝构造 --> 优化为构造 A aa2 = 1; // 构造+拷贝构造 --> 优化为构造 // 19.20 行等价 return 0;}
    int main(){ A ra1 = Func5(); // 拷贝构造+拷贝构造 --> 优化为拷贝构造 cout << \"==============\" << endl; A ra2; ra2 = Func5(); // 不会优化。对象已经定义出来了;而且这里不是拷贝构造,是赋值 return 0;}

    构造、拷贝构造尽量写到1个步骤里

    本篇的分享就到这里了,感谢观看,如果对你有帮助,别忘了点赞+收藏+关注
    小编会以自己学习过程中遇到的问题为素材,持续为您推送文章