C++之类和对象(三)

x33g5p2x  于2021-10-25 转载在 C/C++  
字(15.5k)|赞(0)|评价(0)|浏览(627)

我们首先来看一个关于构造函数和析构函数调用顺序相关的一道题:

  1. class A
  2. {
  3. public:
  4. A()
  5. {
  6. cout<<"A()"<<endl;
  7. }
  8. ~A()
  9. {
  10. cout<<"~A()"<<endl;
  11. }
  12. };
  13. class B
  14. {
  15. public:
  16. B()
  17. {
  18. cout<<"B()"<<endl;
  19. }
  20. ~B()
  21. {
  22. cout<<"~B()"<<endl;
  23. }
  24. };
  25. class C
  26. {
  27. public:
  28. C()
  29. {
  30. cout<<"C()"<<endl;
  31. }
  32. ~C()
  33. {
  34. cout<<"~C()"<<endl;
  35. }
  36. };
  37. class D
  38. {
  39. public:
  40. D()
  41. {
  42. cout<<"D()"<<endl;
  43. }
  44. ~D()
  45. {
  46. cout<<"~D()"<<endl;
  47. }
  48. };

设已经有A,B,C,D,4个类的定义,程序中A,B,C,D析构函数调用顺序为?()

  1. C c;
  2. int main()
  3. {
  4. A a;
  5. B b;
  6. static D d;
  7. return 0;
  8. }

构造函数是定义对象的时候调用的,析构函数是在对象销毁的时候调用的,也就是生命周期结束的时候

栈要符合后进先出,在函数栈帧里面也要符合后进先出的性质,首先我们看a和b,a比b先定义,销毁的时候就是b先销毁,a再销毁,所以现在至少知道了b比a先调用析构函数,我们再来进一步分析,c是一个全局对象,d是一个静态对象,c和d对象在程序的整个运行期间都存在,直到main函数结束,c是在main函数之前就构造处理了, 静态的不是在main函数之前初始化的,而是在第一次调用这个函数时进行初始化的,然后再构造a,再构造b,再构造d,那么析构顺序是怎么样的呢?析构顺序是后定义的先析构,因为c是再main函数进去之前就定义了,所以它是在最后析构,那么a、b、d是怎么样的顺序呢?首先a和b的顺序是b先析构,然后a再析构,那么d是在什么时候析构呢?后定义的不是先析构吗?那么顺序是d、b、a吗?不是的,因为d是静态变量,他是在第一次调用当前域函数的时候初始化,它是静态的,当第二次调用函数的时候就不进行初始化了,它是存在静态区的,它和c一样都是在main函数结束后才销毁的,那么d为什么先比c销毁,是因为它们都在静态区,先定义的后析构,所以析构函数的调用顺序是:b,a,d,c。

再谈构造函数

构造函数体赋值

在创建一个对象时,编译器通过调用构造函数给对象的各个成员进行初始化

此时我们有一个难题:

  1. class A
  2. {
  3. public:
  4. A(int a)
  5. {
  6. _a=a;
  7. }
  8. private:
  9. int_a;
  10. }
  11. class B
  12. {
  13. private:
  14. int _b;//内置类型
  15. A _aa;//自定义类型
  16. };
  17. int main()
  18. {
  19. B b;
  20. }

B类里面有一个内置类型和一个自定义类型的成员变量,我们现在要对它们初始化,假设B类我们不写构造函数,编译器会生成默认构造函数,对自定义类型会调用自定义类型的构造函数,对内置类型不处理。当我们不写B类的构造函数,给A类写一个带参的构造函数时,编译器会报错:

这里本质上是B生成的默认构造函数去调用A的默认构造函数,但是A没有默认构造函数,所以会报错。

我们将A类的构造函数改为默认构造函数就解决了:

对于编译器默认生成的默认构造函数内置类型不处理,自定义类型处理,C++11打了一个补丁:

  1. class B
  2. {
  3. private:
  4. int _b = 1;//内置类型
  5. A _aa;//自定义类型
  6. };

这里是声明,而不是初始化,后面给一个缺省值,当没有处理_b时,就会用默认的,也就是1

但是这样比较尴尬,这里的_b只能初始化为1,那么我们写一个带参的构造函数

  1. B(int a,int b)
  2. {
  3. }

那么这里怎么初始化呢?感觉不好初始化,只要_b好初始化

  1. B(int a,int b)
  2. {
  3. _b = b;
  4. }

关键是_aa怎么初始化呢?这样吗?

  1. B(int a,int b)
  2. {
  3. _aa._a = a;
  4. _b = b;
  5. }

这样是不行的,因为_a在A类是私有的,是不可以访问或者修改的

那么要怎么处理这个问题呢?这样就可以初始化:

  1. B(int a,int b)
  2. {
  3. A aa(a);
  4. _aa = aa;
  5. _b = b;
  6. }

这里定义了A类型aa对象,调用了构造函数初始化了aa对象,再将aa对象赋值给_aa,这里又调用了赋值重载函数,这里是比较麻烦的。

实际上我们也可以定义一个匿名对象进行:

  1. B(int a,int b)
  2. {
  3. _aa = A(a);//匿名对象,只是想临时用用
  4. _b = b;
  5. }

匿名对象的生命周期是定义的这一行

所以我们确定的初始化的解决为:

  1. class A
  2. {
  3. public:
  4. A(int a = 0)
  5. {
  6. _a = a;
  7. cout << "A(int a = 0)" << endl;
  8. }
  9. A& operator=(const A& aa)
  10. {
  11. cout << "operator=(const A& a)" << endl;
  12. if (this != &aa)
  13. {
  14. _a = aa._a;
  15. }
  16. return *this;
  17. }
  18. private:
  19. int _a;
  20. };
  21. class B
  22. {
  23. public:
  24. B(int a, int b)
  25. {
  26. _aa = A(a);//匿名对象,只是想临时用用
  27. _b = b;
  28. }
  29. private:
  30. int _b = 1;
  31. A _aa;
  32. };
  33. int main()
  34. {
  35. B b(10,20);
  36. return 0;
  37. }

可以看到调用了A类的两次构造函数,一次赋值重载函数

可能有的人会疑惑,为什么会有两个构造?

因为在b对象被定义出来以后,有一个自定义类型,有一个内置类型,在B类的构造函数里面它会默认先去调用A的构造:

我们可以通过反汇编可以看到在B的构造函数里面调用了A的构造函数。

我们这样写了一个构造函数完成了初始化,但是可以看到非常麻烦,要初始化这个_aa代价很大,这个是函数体内初始化,接下来我们来看初始化列表初始化:

初始化列表

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

对于没有自定义类型成员时函数体内初始化和使用初始化列表初始化没有什么区别,而在有自定义类型成员时,函数体内初始化改用初始化列表可以提高效率。

因为在有自定义类型成员时,函数体内初始化付出的代价高,而初始化列表初始化呢?

  1. class A
  2. {
  3. public:
  4. A(int a = 0)
  5. {
  6. _a = a;
  7. cout << "A(int a = 0)" << endl;
  8. }
  9. A& operator=(const A& aa)
  10. {
  11. cout << "operator=(const A& a)" << endl;
  12. if (this != &aa)
  13. {
  14. _a = aa._a;
  15. }
  16. return *this;
  17. }
  18. private:
  19. int _a;
  20. };
  21. class B
  22. {
  23. public:
  24. B(int a, int b)
  25. :_aa(a),_b(b)
  26. {
  27. //_b = b;
  28. }
  29. private:
  30. int _b = 1;
  31. A _aa;
  32. };
  33. int main()
  34. {
  35. B b(10,20);
  36. return 0;
  37. }

使用初始化列表初始化只调用了一个构造函数就完成了初始化,不写初始化列表,我们在函数体开始时也要调用构造,只是调用的是默认构造,如果显式的去写,就可以去调用带参的构造

  • 尽量使用初始化列表初始化
    因为就算不写,编译器也会认为是有初始化列表的_b,_aa会使用默认初始化列表进行初始化,可以说初始化列表是成员变量定义的地方,所以建议能使用初始化列表初始化的成员尽量使用初始化列表初始化
  1. class B
  2. {
  3. public:
  4. B(int a, int b)
  5. {
  6. _aa = A(a);//匿名对象,只是想临时用用
  7. _b = b;
  8. }
  9. private:
  10. int _b = 1;
  11. A _aa;
  12. };
  13. int main()
  14. {
  15. B b(10,20);
  16. return 0;
  17. }

尽管我们没有显式的写初始化列表,那么这里也是认为有初始化列表的,_b和_aa会使用默认初始化列表进行初始化,我们也可以认为初始化列表是对象的成员变量定义的地方

我们调式可以看到还没有走函数体内的初始化,他已经被初始化了,所以一般的建议是:能使用初始化列表初始化的成员尽量使用初始化列表进行初始化

  • 有些成员必须使用初始化列表初始化

1.const修饰的变量必须使用初始化列表初始化

  1. class B
  2. {
  3. public:
  4. B(int c)
  5. {
  6. _c=c;
  7. }
  8. private:
  9. const int _c;
  10. int& _ref;
  11. };
  12. int main()
  13. {
  14. B b(1);
  15. return 0;
  16. }

使用函数体内初始化它会报错,为什么呢?
const修饰的变量必须在定义的时候初始化,那么对象的成员在初始化列表定义,已经定义过了就不能再在函数体内改了

2.引用也必须使用初始化列表进行初始化

  1. class B
  2. {
  3. public:
  4. B(int c,int& ref)
  5. :_c(c)
  6. {
  7. _ref=ref;
  8. }
  9. private:
  10. const int _c;
  11. int& _ref;
  12. };
  13. int main()
  14. {
  15. int ref = 10;
  16. B b(1,ref);
  17. return 0;
  18. }

为什么呢?
是因为引用也必须在定义的时候初始化

3.自定义类型成员(该类没有默认构造函数)

  1. class A
  2. {
  3. public:
  4. A(int a)
  5. {
  6. _a = a;
  7. cout << "A(int a = 0)" << endl;
  8. }
  9. private:
  10. int _a;
  11. };
  12. class B
  13. {
  14. public:
  15. B(int c,int& ref)
  16. :_c(c),_ref(ref)
  17. {
  18. }
  19. private:
  20. const int _c;
  21. int& _ref;
  22. A _aa;
  23. };
  24. int main()
  25. {
  26. int ref = 10;
  27. B b(1,ref);
  28. return 0;
  29. }

这里会报错,因为不写,它会调用默认的构造函数,写了就会调用带参的构造函数

总结:

尽量使用初始化列表初始化,因为就算你不显示用初始化列表,成员也会先用初始化列表初始化一遍。
1.
有些成员是必须在初始化列表初始化的。引用、const、没有默认成员函数、的成员
1.
初始化列表和函数体内初始化,可以混着用,互相配合

比如实现带头双向循环链表

  1. //带头双向循环链表
  2. List(): _head(BuyNode(0))
  3. {
  4. //有些初始化用初始化列表完成不了,还得函数体内初始化,所以我们要注意灵活应用
  5. _head->next = _head;
  6. _head->prev = _head;
  7. }
  • 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
  1. class A
  2. {
  3. public:
  4. A(int a)
  5. :_a1(a)
  6. ,_a2(_a1)
  7. {}
  8. void Print()
  9. {
  10. cout<<_a1<<" "<<_a2<<endl;
  11. }
  12. private:
  13. int _a2;
  14. int _a1;
  15. };
  16. int main()
  17. {
  18. A aa(1);
  19. aa.Print();
  20. }

上面这段程序会输出什么呢?

A. 输出1 1

B.程序崩溃

C.编译不通过

D.输出1 随机值

答案是D,为什么呢?

因为成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关,这里是先初始化a2,a2是用a1进行初始化的,但是此时a1还没初始化,a1是随机值,所以a2是随机值,然后再用a初始化a1,故a1为1,所以最后输出1和随机值,在实际中建议声明顺序和初始化列表的顺序保持一致,避免出现这样的问题

explicit关键字

我们首先看下面这个代码:

  1. class A
  2. {
  3. public:
  4. A(int a):_a(a)
  5. {}
  6. private:
  7. int _a;
  8. };
  9. int main()
  10. {
  11. A aa1(1);
  12. return 0;
  13. }

这里就是创建了aa1对象,调用了它的构造函数

还可以这样写:

  1. class A
  2. {
  3. public:
  4. A(int a):_a(a)
  5. {}
  6. private:
  7. int _a;
  8. };
  9. int main()
  10. {
  11. A aa1(1);
  12. //下面本质是一个隐式类型转换
  13. A aa2 = 2;
  14. return 0;
  15. }

第11行是调用构造函数,那么第13行是干什么呢?

它其实是隐式类型转换,int转换成A了,为什么int能转换成A呢?因为它有一个用int去初始化A的构造函数,有这个构造函数就支持,没有这个构造函数就不支持

  1. A aa2 = 2;

怎么证明这一点呢?我们打印出来:

可以看到调用了两次构造函数,并没有调用拷贝构造

232行和234行虽然结果是一样的,都是直接调用构造函数,但是对于编译器而言过程是不一样的,234行是优化后的结果

如果你不想这种隐式类型转换的发生,你可以在构造函数前面加个关键字explicit:

  1. class A
  2. {
  3. public:
  4. explicit A(int a) :_a(a)
  5. {
  6. cout << "A(int a)" << endl;
  7. }
  8. A(const A& a)
  9. {
  10. cout << "A(const A& a)" << endl;
  11. }
  12. private:
  13. int _a;
  14. };
  15. int main()
  16. {
  17. A aa1(1);
  18. //下面本质是一个隐式类型转换
  19. A aa2 = 2;
  20. return 0;
  21. }

此时就会报错无法从int转换为A

上面是一个参数的情况,那么如果是多个参数呢?

  1. class A
  2. {
  3. public:
  4. explicit A(int a,int b) :_a(a)
  5. {
  6. cout << "A(int a)" << endl;
  7. }
  8. A(const A& a)
  9. {
  10. cout << "A(const A& a)" << endl;
  11. }
  12. private:
  13. int _a;
  14. };
  15. int main()
  16. {
  17. A aa1(1,2);
  18. return 0;
  19. }

如果是直接调用构造函数,就传多个参数就可以了,那么第二种怎么写呢?

在C98中是不支持多参数的,但是在C11中支持:

  1. int main()
  2. {
  3. A aa2 = {12};
  4. return 0;
  5. }

C++11支持多个参数时这样写,道理是一样的,编译器会进行优化成只是一个构造

同样地,你不想发生隐式类型转换,你也可以加关键字explicit

static成员

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

我们来看一个面试题:实现一个类,计算程序构造了多少个对象。(构造+拷贝构造)

  1. int countn = 0;
  2. class A
  3. {
  4. public:
  5. A()
  6. {
  7. ++countn;
  8. }
  9. A(const A& a)
  10. {
  11. ++countn;
  12. }
  13. };
  14. A f(A a)
  15. {
  16. A ret(a);
  17. return ret;
  18. }
  19. int main()
  20. {
  21. A a1 = f(A());
  22. A a2;
  23. A a3;
  24. a3 = f(a2);
  25. cout<<countn<<endl;
  26. return 0;
  27. }

可以看到有8个

这样写有一个问题,万一别人写时,把你的countn改了呢?

有什么好的方式让别人不能轻易的改呢?

C++当中有静态变量,成员变量不仅可以是普通的变量,也可以是静态变量

  1. class A
  2. {
  3. public:
  4. A()
  5. {
  6. ++_count;
  7. }
  8. A(const A& a)
  9. {
  10. ++_count;
  11. }
  12. private:
  13. //声明
  14. int _a;
  15. static int _count;
  16. };
  17. //定义初始化
  18. int A::_count = 0;
  19. A f(A a)
  20. {
  21. A ret(a);
  22. return ret;
  23. }
  24. int main()
  25. {
  26. A a1 = f(A());
  27. A a2;
  28. A a3;
  29. a3 = f(a2);
  30. return 0;
  31. }

_a和_count有什么区别呢?

_a存储在定义出的对象中,属于某个对象,而_count存在静态区,属于整个类,也属于每个定义出来的对象共享,跟全局变量比较,他受类域和访问限定符限制,更好体现封装,别人不能轻易修改

注意_count没有存在对象里面

  1. int main()
  2. {
  3. A a1 = f(A());
  4. A a2;
  5. A a3;
  6. a3 = f(a2);
  7. cout << sizeof(A) <<endl;//算的是A定义出来的对象的大小
  8. return 0;
  9. }


可以看到sizeof(A)是4,并没有算_count。

需要注意的是:

静态成员变量不能在构造函数初始化,它在全局位置定义初始化,并且它是私有的,是不能改的

  1. int main()
  2. {
  3. A a1 = f(A());
  4. A a2;
  5. A a3;
  6. a3 = f(a2);
  7. A::_count = 10;//我们假设要修改,会报错
  8. return 0;
  9. }

那么我们要访问_count需要怎么访问呢?这样吗?

  1. cout <<A::_count << endl;
  2. cout << a1._count <<endl;
  3. cout <<a2._count << endl;

这样是不可以的,因为_count是私有的,是不能访问的,那么把_count设为公有的就可以访问了

那么不是公有,这几种方式都不能访问了,那么还可以怎么访问呢?

我们提供一个GetCount成员函数来获取它:

  1. int GetCount()
  2. {
  3. return _count;
  4. }

那么我们就可以这样访问:

  1. int main()
  2. {
  3. A ret;
  4. cout << ret.GetCount()-1 << endl;
  5. //也可以这样写,匿名对象
  6. //cout << A().GetCount()-1 << endl;
  7. return 0;
  8. }

那么我们不想通过创建对象调用这个成员函数怎么办呢?可以把它定义成静态成员函数:

  1. static int GetCount()
  2. {
  3. return _count;
  4. }
  5. int main()
  6. {
  7. cout << A::GetCount() << endl;
  8. //cout << A().GetCount() << endl;//创建对象调用
  9. return 0;
  10. }

使用静态成员函数,突破类域就可以不使用对象调用了,可以使用类指定去访问。

注意:

静态成员函数没有this指针

  1. static int GetCount()
  2. {
  3. _a = 1;//error,没有this指针不可以访问成员变量
  4. return _count;
  5. }

static总结:

C语言:

1、修饰全局变量和全局函数,改变链接属性,只在当前文件可见

2、修饰局部变量,改变生命周期

上面的特性在C中依旧有用,C兼容C的这些特性

C++ :

1、修饰成员变量和成员函数,成员变量属于整个类,所有对象共享,成员函数没有this指针

问题:

1、静态成员函数可以调用非静态成员函数吗?
不可以,因为静态成员函数没有this指针,导致不能调用具体实例对象的成员。静态成员函数的函数形参中没有默认的this指针指向类对象本身。所以当我们调用对象的非静态成员的时候它不能识别该变量

2、非静态成员函数可以调用类的静态成员函数吗?

可以,对于静态成员变量而言,其存在于整个类中,为每个类对象共有,所以就算没有默认的this形参仍然可以识别该静态成员变量

总结:静态的不可以访问非静态的,非静态的可以访问静态的

讲完这些知识,我们来做一道题:

题目描述:
求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

题目思路:

这道题相当于把循环和递归禁掉了,那么有什么方法可以解决呢?可以借助刚才讲的一些特性

  1. class Sum
  2. {
  3. public:
  4. Sum()
  5. {
  6. _ret += _i;
  7. _i++;
  8. }
  9. static int GetRet()
  10. {
  11. return _ret;
  12. }
  13. private:
  14. static int _i;
  15. static int _ret;
  16. };
  17. int Sum::_i = 1;
  18. int Sum::_ret = 0;
  19. class Solution
  20. {
  21. public:
  22. int Sum_solution(int n)
  23. {
  24. Sum a[n];//定义一个n大小的数组,相当于调用了n次构造函数
  25. return Sum::GetRet();
  26. }
  27. };

C++11 的成员初始化新玩法

我们前面说过,我们不写构造函数,编译器对于内置类型默认不会处理,自定义类型会调用它的默认构造函数:

  1. class A
  2. {
  3. public:
  4. A():_a(0)
  5. {}
  6. private:
  7. int _a;
  8. };
  9. class B
  10. {
  11. private:
  12. int _b;
  13. A _aa;
  14. };
  15. int main()
  16. {
  17. B bb;
  18. return 0;
  19. }

内置类型默认不会处理,自定义类型会调用它的默认构造函数,C++在这里打了补丁,给内置类型一个缺省值:

  1. class B
  2. {
  3. private:
  4. int _b = 0;//这里只是声明,不是定义
  5. A _aa;
  6. };

可以看到内置类型处理了。

当然还有很多可以给缺省值,比如:

  1. class B
  2. {
  3. private:
  4. //这里给的都是缺省值,如果在构造函数里显式的给值,这里就没用了
  5. int _b = 0;//这里只是声明,不是定义
  6. int *_p = (int*)malloc(sizeof(int)*10);
  7. //A _aa = A(10);
  8. A _aa = 10;
  9. //静态成员不能在这里给缺省值,因为静态成员不在构造函数初始化,需要在全局位置定义初始化
  10. static int _n =10;//error
  11. };

注意:静态成员不能在这里给缺省值,因为静态成员不在构造函数初始化,需要在全局位置定义初始化

如果在构造函数中我们显示的给值了,就不会用这些缺省值,没有显式给值,就用这些缺省值,这里我们在构造函数当中给_p显式给值了,所以就没有用缺省值:

友元

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

友元函数

我们拿日期类来举例子:

  1. class Date
  2. {
  3. public:
  4. Date(int year, int month, int day)
  5. : _year(year)
  6. , _month(month)
  7. , _day(day)
  8. {}
  9. prvate:
  10. int _year;
  11. int _month;
  12. int _day
  13. };

我们想像内置类型一样去输入输出:

  1. int main()
  2. {
  3. Date d1;
  4. cin >> d1;//把d1这个对象插入到这个流当中去,输入
  5. cout << d1;//把d1这个对象插入到这个流当中去,输出
  6. return 0;
  7. }

那么我们需要重载<<运算符和>>运算符,那么我们想一下这两个运算符的操作数是几个呢?是两个,cin是istream类型,cout是ostream类型,我们首先重载<<运算符:

  1. void operator<<(ostream& out)
  2. {
  3. out << _year <<"/"<<_month<<"/"<<_day<<endl;
  4. }

但是这样编译不过,为什么呢?

运算符重载,运算符有几个操作数,重载函数就有几个参数
如果是两个操作数,左操作数是第一个参数,右操作数是第二个参数,operator<<写成成员函数,隐含的this指针默认占据了第一个位置,对象就要做左操作数。那么用起来就不符合流特性,虽然可以用,但是不符合运算符原来的用法和特性

相当于是这样:

  1. void operator<<(Date* this,ostream& out)
  2. {
  3. out << _year <<"/"<<_month<<"/"<<_day<<endl;
  4. }

而我们调用时,是这样写的:

  1. cout << d1;

这里顺序冲突了,给重载成员函数的第一个位置传了cout,而不是d1对象,本来是应该给第一个传d1对象,第二个传cout,我们这样写就可以成功:

  1. d1 << cout;
  2. //d1.operator<<(cout);

但是这样影响了它的特性,那么怎么办呢?我们需要写成非成员函数,out做左操作数,类对象做第二个参数:

  1. void operator<<(ostream& out,const Date& d)
  2. {
  3. out<<d._year<<d._month<<d._day<<endl;
  4. }

但是现在我们又不能访问私有成员,怎么办呢?我们可以写GetYear,GetMonth,GetDay,这些成员函数进行访问,但是这样比较麻烦,那么怎么样还可以解决呢?在C++中有一个友元可以解决这个问题,我们想要访问这个类的私有,那就把这个成员函数设置成这个类的友元:

  1. class Date
  2. {
  3. friend void operator<<(ostream& _cout,const Date& d);
  4. public:
  5. Date(int year, int month, int day)
  6. : _year(year)
  7. , _month(month)
  8. , _day(day)
  9. {}
  10. };
  11. prvate:
  12. int _year;
  13. int _month;
  14. int _day
  15. };
  16. void operator<<(ostream& out,const Date& d)
  17. {
  18. out<<d._year<<d._month<<d._day<<endl;
  19. }

但是现在写的也是不行的,为什么呢?<<运算符是不是还支持连续输出:

  1. cout << d1 << d2;

所以我们是需要返回值的:

  1. class Date
  2. {
  3. friend ostream& operator<<(ostream& _cout,const Date& d);
  4. public:
  5. Date(int year, int month, int day)
  6. : _year(year)
  7. , _month(month)
  8. , _day(day)
  9. {}
  10. };
  11. prvate:
  12. int _year;
  13. int _month;
  14. int _day
  15. };
  16. ostream& operator<<(ostream& out,const Date& d)
  17. {
  18. out<<d._year<<d._month<<d._day<<endl;
  19. return out;
  20. }

我们就顺理成章的可以写>>运算符的重载:

  1. class Date
  2. {
  3. friend istream& operator>>(istream& in, Date& d);
  4. public:
  5. Date(int year, int month, int day)
  6. : _year(year)
  7. , _month(month)
  8. , _day(day)
  9. {}
  10. };
  11. prvate:
  12. int _year;
  13. int _month;
  14. int _day
  15. };
  16. istream& operator>>(istream& in, Date& d)
  17. {
  18. in >> d._year >> d._month >> d._day;
  19. return in;
  20. }

那么这里就可以使用了:

  1. int main()
  2. {
  3. Date d1;
  4. cin >> d1;//把d1这个对象插入到这个流当中去,输入
  5. cout << d1;//把d1这个对象插入到这个流当中去,输出
  6. return 0;
  7. }

总结:
为了让cout在第一个参数,左操作数,我们只能把<<运算符重载写成全局的,其次就是operator<<搞成友元,可以在类中访问私有,但是operator<<不是必须要是友元,还有其他方式,比如通过写成员函数来访问。

  • 友元函数可访问类的私有和保护成员,但不是类的成员函数
    声明为友元函数的函数能在该类中访问私有和保护成员,它并不是类的成员函数

  • 友元函数不能用const修饰
    回答:不能,在类中,const 修饰函数是修饰函数的什么?当然是类函数中的this指针了,那么友元函数有没有this指针呢?当然没有this指针了,所以友元函数不能用const修饰。

  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制

  • 一个函数可以是多个类的友元函数

例如:

  1. class B;
  2. class A
  3. {
  4. friend void f(const A& aa, const B& bb);
  5. public:
  6. A():_a(10)
  7. {}
  8. private:
  9. int _a;
  10. };
  11. class B
  12. {
  13. friend void f(const A& aa, const B& bb);
  14. public:
  15. B():_b(20)
  16. {}
  17. private:
  18. int _b;
  19. };
  20. void f(const A& aa, const B& bb)
  21. {
  22. cout << aa._a << endl;
  23. cout << bb._b << endl;
  24. }
  25. int main()
  26. {
  27. A aa;
  28. B bb;
  29. f(aa, bb);
  30. return 0;
  31. }

这里需要注意我们在A类中将该函数定义为友元时,我们需要在这之前先声明B类,因为编译器不知道B这个符号是什么。

  • 友元函数的调用与普通函数的调用和原理相同

友元类

友元类的所有成员函数都可以是另一个类的友元函数,这些友元函数都可以访问另一个类中的私有成员

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

  • 友元关系不能传递
    如果B是A的友元,C是B的友元,则不能说明C是A的友元

友元类:

  1. #include<iostream>
  2. using namespace std;
  3. class Date;
  4. class Time
  5. {
  6. friend Date;//友元类
  7. public:
  8. Time(int hour = 0, int minute = 0, int second = 0)
  9. :_hour(hour), _minute(minute), _second(second)
  10. {}
  11. void Print()
  12. {
  13. cout << _hour << "/" << _minute << "/" << _second << endl;
  14. }
  15. private:
  16. int _hour;
  17. int _minute;
  18. int _second;
  19. };
  20. class Date
  21. {
  22. public:
  23. Date(int year,int month,int day)
  24. :_year(year),_month(month),_day(day),_t(11,26,30)
  25. {}
  26. void Print()
  27. {
  28. cout << _year << "/" << _month << "/" << _day <<" "<< _t._hour <<":"<< _t._minute<<":" << _t._second << endl;//Time类是Date类的友元,可以访问私有成员
  29. }
  30. private:
  31. int _year;
  32. int _month;
  33. int _day;
  34. Time _t;
  35. };
  36. int main()
  37. {
  38. Date d1(2021, 10, 18);
  39. d1.Print();
  40. return 0;
  41. }

内部类

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

特性:

  1. 内部类可以定义在外部类的public、protected、private都是可以的。
  1. class A
  2. {
  3. private:
  4. static int k;
  5. int h;
  6. public:
  7. //protect:
  8. //private:
  9. class B //B天生就是A的友元
  10. {
  11. private:
  12. int _b;
  13. };
  14. };
  1. 内部类天生就是外部类的友元,内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
  1. class A
  2. {
  3. private:
  4. static int k;
  5. int h;
  6. public:
  7. class B //B天生就是A的友元
  8. {
  9. public:
  10. void foo( const A& a)
  11. {
  12. cout << k << endl; //OK
  13. cout << a.h << endl; //OK
  14. }
  15. private:
  16. int _b;
  17. };
  18. };
  19. int main()
  20. {
  21. A::B b;
  22. b.foo(A());
  23. return 0;
  24. }

  1. sizeof(外部类)=外部类,和内部类没有任何关系。
  1. class A
  2. {
  3. private:
  4. static int k;
  5. int h;
  6. public:
  7. class B //B天生就是A的友元
  8. {
  9. public:
  10. void foo( const A& a)
  11. {
  12. cout << k << endl; //OK
  13. cout << a.h << endl; //OK
  14. }
  15. private:
  16. int _b;
  17. };
  18. };
  19. int main()
  20. {
  21. cout << sizeof(A) << endl;
  22. A::B b;
  23. b.foo(A());
  24. return 0;
  25. }

这里计算A类型对象大小的时候,不考虑B类。因为B作为A的内部类,跟普通类并没有什么区别,只是定义在A的内部,他受到A的类域的限制和访问限定符的限制

理解完内部类,我们再来看一下我们前面做过的那道题

题目描述:
求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

题目思路:

这道题相当于把循环和递归禁掉了,那么有什么方法可以解决呢?这里我们可以把Sum类设置为Solution类的内部类,这里可以更好的体现封装性

  1. class Solution
  2. {
  3. private:
  4. class Sum//内部类天生是外部类的友元
  5. {
  6. public:
  7. Sum()
  8. {
  9. _ret += _i;//Sum是Solution的友元,所以可以访问它的私有成员
  10. _i++;
  11. }
  12. };
  13. static int _i;
  14. static int _ret;
  15. public:
  16. int Sum_Solution(int n)
  17. {
  18. Sum a[n];//定义一个n大小的数组,相当于调用了n次构造函数
  19. return _ret;
  20. }
  21. };
  22. int Solution::_i = 1;
  23. int Solution::_ret = 0;

因为内部类天生是外部类的友元,所以我们把数据放在外部类,因为内部类可以访问外部类的数据,外部类不可以访问内部类,因为我们为了更好的体现封装性,我们可以把内部类设置为private,这样外面就不能用它了,只让外部类用它的功能就可以了

下面我们再看一道题:

题目描述:
输入一个日期,输出是这一年的第几天

输入描述:

输入一行,每行空格分割,分别是年,月,日

输出描述:

输出是这一年的第几天

  1. #include<iostream>
  2. using namespace std;
  3. int main()
  4. {
  5. //monthDays[i]存的是1+2+3+...i月的天数
  6. static int monthDays[13]={0,31,59,90,120,151,181,212,243,273,304334,365}
  7. int year,month,day;
  8. cin>>year>>month>>day;
  9. int n =monthDays[month-1]+day;//不是闰年的天数=monthDays数组的month-1存的元素+day
  10. //闰年2月多一天
  11. if (month > 2 && ((year%4==0 && year%100!=0) || year %400 == 0))
  12. {
  13. n++;
  14. }
  15. return n;
  16. }

再次理解封装

C++是基于面向对象的程序,面向对象有三大特性即:封装、继承、多态。
C++通过类,将一个对象的属性与行为结合在一起,使其更符合人们对于一件事物的认知,将属于该对象的所有东西打包在一起;通过访问限定符选择性的将其部分功能开放出来与其他对象进行交互,而对于对象内部的一些实现细节,外部用户不需要知道,知道了有些情况下也没用,反而增加了使用或者维护的难度,让整个事情复杂化。

例如我们在实现栈时:

  1. struct Stack
  2. {
  3. int* a;
  4. int top;
  5. int capacity;
  6. }
  7. int StackTop(Stack* ps)
  8. {
  9. return ps->a[ps->top-1];
  10. }
  11. int main()
  12. {
  13. //正确的访问方式
  14. Stack st;
  15. StackTop(&st);
  16. //错误的访问方式
  17. st.a[st.top];//如果访问的人自己想自由获取数据,又不清楚top规则,可能就会出问题
  18. return 0;
  19. }

在现实中,总有人不守规矩,我们本来有正确的接口去获取top,但是总有人自己想自由获取数据,又不清楚top规则,可能就会出问题。

而类就不一样了,这就体现了封装性:

  1. class Stack
  2. {
  3. public:
  4. int Top()
  5. {
  6. return _a[_top-1];
  7. }
  8. private:
  9. int*_a;
  10. int _top;
  11. int _capacity;
  12. };
  13. int main()
  14. {
  15. //正确
  16. Stack st;
  17. st.Top();
  18. //错误
  19. st.a[st._top]//不可以,压根不给你犯错的机会,因为_top是私有成员,不能访问0【0-
  20. return 0;
  21. }

使用类时,将_top设置为私有成员,当在外部访问时,编译器就会报错,因为它是私有的,不能访问

再次理解面向对象

可以看出面向对象其实是在模拟抽象映射现实世界。

比如我们在打英雄联盟时,我们选定的英雄就是对象,我们的战绩,输出,承伤等等就是这个对象的属性,在游戏中移动,攻击其实就是方法。

欢迎大家学习交流!

相关文章

最新文章

更多