前言
来到类和对象最后一个章节,这里的难度已经极大程度的降低了
再探构造函数
概念概述
- 之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有一种方式,就是初始化列表,初始化列表的使用方式是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个 “成员变量” 后面跟一个放在括号中的初始值或表达式。
- 每个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。
- 引用成员变量,const 成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始化,否则会编译报错。
- C++11 支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的。
- 尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会走初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++ 并没有规定。对于没有显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造会编译错误。
- 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持一致。
- 初始化列表总结:无论是否显式写初始化列表,每个构造函数都有初始化列表;无论是否在初始化列表显示初始化,每个成员变量都要走初始化列表初始化。
成员变量初始化列表逻辑
再探构造函数存在的意义
//比如我们写一个栈,这里就不需要写构造函数,因为编译器默认生成队列的构造函数,调用了stack的默认构造,完成了两个成员函数的初始化
//但是如果栈的默认构造是没有的情况下,此时怎么处理?所以就需要延伸到再探构造函数
初始化列表的格式
之前我们进行初始化是函数体的初始化,现在称之为初始化列表的初始化
初始化列表的使用方式是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个 “成员变量” 后面跟一个放在括号中的初始值或表达式。
使用的格式记住就可以
哪些成员必须采用初始化列表(没有默认构造函数的,需要使用初始化列表)
//构造函数 Date(int year = 1901, int month = 1, int day = 1); 构造函数 //Date::Date(int year, int month, int day) //{ // _year = year; // _month = month; // _day = day; //} //构造函数,初始化列表的实现 Date::Date(int year, int month, int day) :_year(111) , _month(111) , _day(111) {} int main() { // 明确传递参数,使用带参数的构造函数和初始化列表进行初始化 Date d1(2024, 9, 27); // 不传递参数,使用默认参数值和初始化列表进行初始化 Date d2; return 0; } //不是单纯地调用默认构造函数,而是根据是否传递参数来决定使用带参数的构造函数(带默认参数),并且始终会使用初始化列表进行初始化。 //如果传递参数调用构造函数,如Date d1(2024, 9, 27);,会优先使用初始化列表,按照给定的参数值初始化成员变量_year、_month和_day。 //如果不传递参数,如Date d2;,则会使用构造函数中的缺省参数值,并同样通过初始化列表来初始化成员变量。
哪些成员必须采用初始化列表(const修饰的成员变成必须使用初始化列表)
一,const 成员函数的特性
const 成员函数承诺不修改对象的数据成员,即它保证了在函数执行期间,对象的状态不会被改变。这意味着 const 成员函数应该只能读取对象的状态,而不能进行任何可能改变对象状态的操作。
二,函数体初始化与初始化列表的区别
- 函数体初始化:在函数体内部进行成员变量的初始化是通过赋值操作来完成的。这意味着先调用默认构造函数(如果有)创建成员变量,然后再对其进行赋值。这种方式可能会导致对象被临时创建和赋值两次,增加了不必要的开销,并且在一些情况下可能违反 const 成员函数的不修改对象状态的承诺。
- 初始化列表:初始化列表在对象创建时直接初始化成员变量,避免了先调用默认构造函数再赋值的过程。对于 const 成员变量和引用成员变量,必须在初始化列表中进行初始化,因为它们不能在创建后被重新赋值。对于普通成员变量,使用初始化列表也可以提高效率,并且更符合 C++ 的初始化语义。
代码实现:这里主要看成员变量_y
#include<iostream> using namespace std; class Date { public: Date(int& z); Date(int x, int& z); Date(int x, int y, int& z); void _print() { cout << _x << "/" << _y << "/" << _z << endl << endl; } private: int _x; const int _y; int& _z; }; //再探构造函数1 Date::Date(int& z) :_x(111) , _y(111) , _z(z) {} //再探构造函数2 Date::Date(int x, int& z) :_x(222) , _y(222) , _z(z) {} //再探构造函数3 Date::Date(int x, int y, int& z) : _y(333) ,_z(z) { _x = x; } int main() { //再探构造函数1 int i1 = 0; Date d1(i1); d1._print(); cout << i1 << endl << endl; //再探构造函数2 int i2 = 0; Date d2(1, i1); d2._print(); cout << i2 << endl << endl; //再探构造函数3 int i3= 0; Date d3(1, 1, i3); d3._print(); cout << i3 << endl; return 0; }
哪些成员必须采用初始化列表(引用成员变量的初始化)
一、引用的特性
引用在 C++ 中必须在定义时被初始化,并且一旦初始化后就不能再绑定到其他对象。引用本质上是一个对象的别名,它总是指向一个特定的对象,不能被重新赋值为指向另一个不同的对象。
二、构造函数的执行过程
- 当一个对象被创建时,构造函数首先执行初始化列表来初始化成员变量。如果不在初始化列表中初始化引用成员变量,那么在构造函数体中就没有机会再对其进行初始化了,因为此时引用必须已经被绑定到一个对象。
- 如果试图在构造函数体中对引用成员变量进行赋值,这会被编译器视为重新绑定引用,而这是不允许的,会导致编译错误。
三、左值和右值的概念
- 左值(lvalue):代表一个有明确内存地址、可以取地址且有可持久性的值。通常可以出现在赋值语句的左侧,例如变量名、解引用的指针等。例如,
int a = 10;
中的a
就是一个左值,因为它有明确的内存地址,可以通过&a
取地址,并且在程序的生命周期内持续存在。引用成员变量的初始化就是不修改的左值。- 右值(rvalue):通常是临时的值,没有明确的内存地址可获取,或者是即将被销毁的值。例如,字面常量、临时对象、函数返回的临时值等。例如,
int b = 15 + 20;
这里15 + 20
的结果就是一个右值,它是一个临时值,没有独立的内存地址可获取,在表达式结束后可能就会被销毁。代码实现
#include<iostream> using namespace std; class Date { public: Date(int& z); Date(int x, int& z); Date(int x, int y, int& z); void _print() { cout << _x << "/" << _y << "/" << _z << endl << endl; } private: int _x; const int _y; int& _z; }; //再探构造函数1 Date::Date(int& z) :_x(111) , _y(111) , _z(z) {} //再探构造函数2 Date::Date(int x, int& z) :_x(222) , _y(222) , _z(z) {} //再探构造函数3 Date::Date(int x, int y, int& z) : _y(333) ,_z(z) { _x = x; } int main() { //再探构造函数1 int i1 = 0; Date d1(i1); d1._print(); cout << i1 << endl << endl; //再探构造函数2 int i2 = 0; Date d2(1, i1); d2._print(); cout << i2 << endl << endl; //再探构造函数3 int i3= 0; Date d3(1, 1, i3); d3._print(); cout << i3 << endl; return 0; }
初始化列表的注意事项(初始化的顺序)
初始化列表的注意事项(尽量都使用初始化列表进行初始化)
这里建议尽量使用初始化列表进行初始化,因为在C++里面,他是有一套构造流程的,而且对于C++来讲,为什么比其他语言快,不仅仅的这个语言本身的特点,还是在书写的时候,我们会更加注意程序的设计
初始化列表的注意事项(建议声明和顺序保持一致)
这里也是一个需要注意的点
因为编译器进行初始化的时候,是按照头文件进行初始化的,不是你实现的顺序来进行初始化的
隐式类型转换
概念概述
• C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
• 构造函数前面加 explicit 就不再支持隐式类型转换。
• 类类型的对象之间也可以隐式转换,需要相应的构造函数支持。
单参数构造
什么是单参数构造,顾名思义就是只有一个参数的构造函数
这里我们可以拿日期类来举例单参数构造
比如这里我们计算日期类的日期指之间相差多少的问题
这里的日期类就产生了:单参数构造的问题,因为是一个参数,所以在调用的时候编译器不确定你这里是需要传递一个参数还是需要隐式类型转换转化为类 进行拷贝构造。从而产生因为单参数构造产生的调用歧义的问题。
想要学会这个隐式类型转换的问题,还是需要对日期类有一定的了解,建议观看一下尤其是日期类之间的相差天数,这里就涉及到调用歧义的问题
日期类的实现(C++)-CSDN博客https://blog.csdn.net/Jason_from_China/article/details/142516352日期类的实现- 计算日期之间相差多少天-解决单参数构造-CSDN博客https://blog.csdn.net/Jason_from_China/article/details/142590750单参数构造
这里就涉及到隐式类型转换,整因为本来应该传递类类型的数值,但是这里我们传递了一个整数类型,此时就会导致创建一个临时对象,但是临时对象具备常性,就会导致传参的时候需要加上const不然会导致权限的放大,从而导致报错
这里涉及一个比较典型的隐式类型转换,从而导致定义歧义,所以我们加上const或者在构造函数前加上explicit
多参数构造
什么是多参数构造:顾名思义就是有多个参数的构造函数,这里和单参数构造是一样的
下面我们进行多参数构造的调用歧义进行讲解
//初始化列表 class MyClass { public: MyClass(int a1 = 0, char a2 = 0, int a3 = 0) : _a1(a1) , _a2(a2) , _a3(a3) {} void _print() { cout << _a1 << "/" << _a2 << "/" << _a3 << endl; } private: int _a1; int _a2; int _a3; }; class A { public: A(int a1 = 0, char a2 = 0, int a3 = 0) : _a1(a1) , _a2(a2) , _a3(a3) {} void _print() { cout << _a1 << "/" << _a2 << "/" << _a3 << endl; } class B { friend A; public: B(const A aa) : _b1(aa._a1) , _b2(aa._a2) , _b3(aa._a3) {} void _print() { cout << _b1 << "/" << _b2 << "/" << _b3 << endl; } private: int _b1; int _b2; int _b3; }; private: int _a1; int _a2; int _a3; }; //多参数构造 //隐式类型转换 int main() { MyClass aa1 = { 5,5,5 }; aa1._print(); MyClass aa2 = { 5 ,'1'}; aa2._print(); MyClass aa3 = { 5 }; aa3._print(); printf("\n"); A a1 = { 5,5,5 }; a1._print(); A a2 = { 5,5 }; a2._print(); A a3 = { 5 }; a3._print(); printf("\n"); A::B b1 = a3; b1._print(); return 0; }
拿这个代码举例来讲,
一、
MyClass
的构造函数调用
MyClass aa3 = { 5 };
:这里只有一个整数参数,但是构造函数MyClass(int a1 = 0, char a2 = 0, int a3 = 0)
有三个参数,编译器可能不确定是将这个整数赋值给第一个参数_a1
,还是进行某种隐式转换后赋值给其他参数,从而产生调用歧义。- 这里的代码就是类类型的拷贝,直接拷贝给B的类,也就是在隐式类型转换的时候,会 产生这样的情况
A::B b1 = a3;
b1._print();二、
A
的构造函数调用
A a2 = { 5, 5 };
:这里提供了两个参数,但是构造函数A(int a1 = 0, char a2 = 0, int a3 = 0)
有三个参数,编译器可能不确定如何分配这两个参数,可能会尝试一些隐式转换,但具体行为不确定,那么此时代码简单不会产生调用歧义,但是当代码复杂的时候,编译器就不知道是否需要转化为类类型进行拷贝,从而产生调用歧义。
A a3 = { 5 };
:与MyClass aa3
类似,只有一个整数参数,编译器不确定如何将其分配给三个参数,那么此时代码简单不会产生调用歧义,但是当代码复杂的时候,编译器就不知道是否需要转化为类类型进行拷贝,从而产生调用歧义。总的来说,当构造函数的参数数量与提供的初始化参数数量不匹配时,编译器可能会尝试进行隐式转换或不确定的参数分配,从而导致调用歧义。为了避免这种情况,可以考虑使用更明确的初始化方式、提供适当的单参数构造函数或者使用
explicit
关键字来防止不必要的隐式转换。
类型转换的意义 (类类型在构造的时候,可以直接输入整形进行构造,而不是需要一个类才能进行构造)
注意事项:
在接收参数的时候,这里需要加上const,因为本来应该传递类类型的数值,但是这里我们传递了一个整数类型,此时就会导致创建一个临时对象,但是临时对象具备常性,就会导致传参的时候需要加上const不然会导致权限的放大,从而导致报错
为什么产生权限放大的解释:
类型转换的意义 (自定义类型和自定义类型之间转换)(利用成员函数)(const的使用需要注意)
class C { public: C(int c1 = 1, int c2 = 1) :_c1(c1) , _c2(c2) {} //int getC_c1(C* const this)//隐藏的this指针,但是这里隐藏的this指针是修饰指针的,不是修饰内容的 int getC_c1() { return _c1; } int getC_c2() { return _c2; } private: int _c1 = 1; int _c2 = 2; }; class D { public: //在C类的成员函数没有加上const修饰的时候,是不能用const修饰的,因为这里你加上const就会导致,调用之前是不可以修改的,调用的时候C的类的可以修改的左值,回来就会导致c.getC_c1()变成可以修改的左值,但是实际是不可以修改的//D(const C& c) D( C& c) :_d1(c.getC_c1()) , _d2(c.getC_c2()) {} void _print() { cout << _d1 << "/" << _d2 << endl; } private: int _d1; int _d2; }; //多参数构造 //隐式类型转换 int main() { C c1 = { -1,-1}; const D& d1 = c1; //D d1 = c1;//这里是报错的 return 0; }
这里我们需要看的是调用的两行, C c1 = { -1,-1}; const D& d1 = c1;,这里涉及到的是隐式类型转换,绑定的问题。在拷贝的时候,这里是需要加上const的,因为在赋值的时候会产生隐式类型转换,隐式类型转化会创建一个临时变量,存放这个类型从而完成拷贝构造,这里需要注意,临时对象具备常性,所以我们需要用const进行修饰,不然会导致权限放大的问题
在绑定的时候我们也可以发现,c1和d1的地址是不一样的(本来应该是一样的),因为我们绑定的地址不是创建对象的地址,而是临时对象的地址,所以会导致引用(&)之后,地址绑定的是临时变量,所以会导致地址的不同
注意事项;
- 在D的类里面,如果我们直接给构造函数D(C& c)加上const,D(const C& c),此时会报错(权限放大)
因为C的成员函数没有加上const修饰,从而导致权限放大的问题,因为这里是隐藏this指针的
//int getC_c1(C* const this)//此时内容是可以修改的,指针是不可以修改的
const C& c的类被修饰之后C的内容是不可以被修改了,此时int getC_c1()和int getC_c2()是没有修饰的内容是可以修改的,所以一调用就导致了权限放大的问题在利用自定义之间进行转换,我们需要利用成员函数(逻辑明确,封装安全),内部类是可以直接进行转换的,外部类是不能直接进行转换的
如下所示:
class C { public: C(int c1 = 1, int c2 = 1) :_c1(c1) , _c2(c2) {} int getC_c1()const { return _c1; } int getC_c2()const { return _c2; } private: int _c1 = 1; int _c2 = 2; }; class D { public: D(const C& c) :_d1(c.getC_c1()) , _d2(c.getC_c2()) {} void _print() { cout << _d1 << "/" << _d2 << endl; } private: int _d1; int _d2; }; //多参数构造 //隐式类型转换 int main() { C c1 = { -1,-1}; const D& d1 = c1; //D d1 = c1; return 0; }
类型转换的意义 (自定义类型和自定义类型之间转换)(内部类)
- 内部类(这里我们发现是可以直接进行转化的)
转换的方式
转为临时对象:
- 当发生隐式类型转换时,通常会创建一个临时对象。例如,当一个函数期望一个类型为
B
的对象,但传递了一个类型为A
的对象时,编译器会调用B
的转换构造函数创建一个临时的B
对象来进行参数传递。- 这种情况下,是通过调用转换构造函数来创建一个新的临时对象,该临时对象的生命周期由其所在的表达式决定。
直接拷贝构造:
- 如果已经有一个自定义类型的对象,并且要将其转换为另一个自定义类型的对象,同时存在合适的构造函数,可能会进行直接的拷贝构造。
- 例如,如果有一个
A
类型的对象a
,并且B
类有一个接受A
类型对象作为参数的拷贝构造函数,那么可以通过拷贝构造将a
转换为B
类型的对象。这种情况下,有的编译器,会直接使用构造函数,来构造目标对象,而不是创建临时对象。
static静态
概念概述
- 用 static 修饰的成员变量,称之为静态成员变量,静态成员变量一定要在类里面生命,在类外进行初始化。
- 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
- 用 static 修饰的成员函数,称之为静态成员函数,静态成员函数没有 this 指针。
- 静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有 this 指针。
- 非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
- 突破类域就可以访问静态成员,可以通过类名::静态成员或者对象.静态成员来访问静态成员变量和静态成员函数。
- 静态成员也是类的成员,受 public、protected、private 访问限定符的限制。
- 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。
static的使用1:
- 静态成员函数的使用是很结合构造和析构的,静态成员函数是可以直接突破类域来访问静态成员函数
- 不是静态成员函数就不能突破类域直接访问,我们需要创建一个对象,利用对象访问成员函数
- 如果不设置静态成员函数,那么你需要访问成员变量,就需要创建一个对象才能访问成员函数,从而导致麻烦。所以我们可以直接设置为静态成员函数。
- 这里有一个注意事项,就是不是静态成员变量,不能用静态成员函数访问,不然会导致报错
static的使用2:
这里我使用vs编译器,vs编译器不支持可边长数组,所以代码有点不一样,这里需要注意,只有静态成员变量才能直接突破类域访问,不是静态成员函数,是需要创建类的。
#include<iostream> using namespace std; //求1+2......+n//不能使用乘除 class sum { public: sum() { _ret += _i; ++_i; } //获取ret最终的结果 static int _GetRet() { return _ret; } private: static int _i; static int _ret; }; int sum::_i = 1; int sum::_ret = 0; class solution { public: int _solution(int n) { for (int i = 0; i < n; i++) { //创建n次构造函数,n次构造函数++会依次递加 sum s; } //创建n次数之后,我们计算出来数值,此时我们可以选择数值来进行接收 int ret = sum::_GetRet(); //打印数值 cout << ret << endl; //返回数值 return sum::_GetRet(); } }; int main() { solution s; int ret = s._solution(100);//这里接收进行验证,发现是正确的 cout << ret << endl;//打印出来 return 0; }
类
sum
成员变量:
static int _i
:静态成员变量,用于记录当前要累加的数字。初始化为 1。static int _ret
:静态成员变量,用于存储累加的结果。初始化为 0。构造函数:
- 每次创建
sum
类的对象时,构造函数被调用。在构造函数中,将当前的_i
值累加到_ret
中,然后将_i
的值自增。这样,每次创建对象就相当于进行一次累加操作。静态成员函数
_GetRet()
:
- 用于获取
_ret
的值,即累加的结果。类
solution
- 成员函数
_solution(int n)
:
- 这个函数接受一个整数
n
,目的是计算从 1 加到n
的和。- 通过一个循环创建
n
次sum
类的对象。每次创建对象时,sum
类的构造函数会自动进行累加操作。- 最后,通过
sum::_GetRet()
获取累加的结果,并将其赋值给ret
,然后打印和返回这个结果。
static的使用3:
构造函数:C是全局变量先进行构造,其次就是局部变量依次进行构造
析构函数:先析构局部变量,先构造的后析构,所以先析构B,其次是A,最后是D,全局变量生命周期伴随函数的结束
友元函数
概念概述
- 友元提供了一种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类声明的前面加 friend,并且把友元声明放到一个类的里面。
- 外部友元函数可访问类的私有和保护成员,友元函数仅仅是一种声明,他不是类的成员函数。
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
- 一个函数可以是多个类的友元函数。
- 友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员。
- 友元类的关系是单向的,不具有交换性,比如 A 类是 B 类的友元,但是 B 类不是 A 类的友元。
- 友元类关系不能传递,如果 A 是 B 的友元,B 是 C 的友元,但是 A 不是 C 的友元。
- 有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
- 友元函数比较简单,这里不做过多的举例和讲解,简单的说就是,外面的函数可以访问类的成员变量,你只需要在类里面的公有区域friend+函数全称,就可以了
友元函数的使用1
日期类的实现
日期类的实现(C++)-CSDN博客https://blog.csdn.net/Jason_from_China/article/details/142516352
友元函数的使用2:
这里我直接拿着我画图的图解来就可以了,讲述的比较清楚
内部类
概念概述
- 如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
- 内部类默认是外部类的友元类。
- 内部类本质也是一种封装,当 A 类跟 B 类紧密关联,A 类实现出来主要就是给 B 类使用,那么可以考虑把 A 类设计为 B 的内部类,如果放到 private/protected 位置,那么 A 类就是 B 类的专属内部类,其他地方都用不了。
内部类的使用1
正常代码
class A { public: A(); ~A(); private: }; class B { public: B(); ~B(); private: };
内部类的代码
#include<iostream> using namespace std; class A { public: A(int a1 = 1, int a2 = 1) :_a1(a1) ,_a2(a2) {}; class B { public: //错误写法,内部类和外部类是相对独立的,是不能直接访问的 //B(int b1 = _a1, int b2 = _a2) //B() //{} 直接访问类(这里没有默认构造) //B(const A a) // :_b1(a._a1) // ,_b2(a._a2) //{} //默认构造写法 B(int b1 = 2, int b2 = 2) :_b1(b1) ,_b2(b2) {} void _print() { cout << "内部类默认是外部类的友元函数" << endl; } private: int _b1; int _b2; }; private: int _a1 = 1; int _a2 = 1; }; int main() { A a; A::B b; return 0; }
注意事项:
- 内部类和外部类是相对独立的,外部类构造时,不能直接将构造的数值传递给内部类。外部类先构造,但在外部类构造函数执行时,内部类的对象可能还未创建,内部类的作用域在外部类内部,外部类构造函数不能直接访问还未创建的内部类对象的成员。
下图我们就可以看出来,我们直接从外部类构造的数值去初始化内部类是不能成功的,需要默认构造函数但是我们加上默认构造是可以的,说明,内部类和外部类是相对独立的
- 内部类默认是外部类的友元函数(内部类的调用受到访问限定符的限制)
这里我们发现A类,突破类域就可以访问B,但是B不能说突破类域访问A
- 如果你需要把外部类的构造给内部类使用,那么我们可以采取get成员函数的方式,或者静态成员变量的方式来进行使用
#include<iostream> using namespace std; class A { public: A(int a1 = 1, int a2 = 1) :_a1(a1) ,_a2(a2) {}; int geta1() { return _a1; } class B { public: //直接访问类(这里没有默认构造)(可以接收参数) B(const A a) :_b1(a._a1) ,_b2(a._a2) {} //默认构造写法 B(int b1 = 2, int b2 = 2) :_b1(b1) ,_b2(_b2) {} private: int _b1; int _b2; }; private: int _a1 = 1; int _a2 = 1; }; int main() { A a; int ret1 = a.geta1(); A::B b = { ret1 }; return 0; }
内部类的使用2:
没有学习之前:
这里我使用vs编译器,vs编译器不支持可边长数组,所以代码有点不一样
#include<iostream> using namespace std; //求1+2......+n//不能使用乘除 class sum { public: sum() { _ret += _i; ++_i; } //获取ret最终的结果 static int _GetRet() { return _ret; } private: static int _i; static int _ret; }; int sum::_i = 1; int sum::_ret = 0; class solution { public: int _solution(int n) { for (int i = 0; i < n; i++) { //创建n次构造函数,n次构造函数++会依次递加 sum s; } //创建n次数之后,我们计算出来数值,此时我们可以选择数值来进行接收 int ret = sum::_GetRet(); //打印数值 cout << ret << endl; //返回数值 return sum::_GetRet(); } }; int main() { solution s; int ret = s._solution(100);//这里接收进行验证,发现是正确的 cout << ret << endl;//打印出来 return 0; }
类
sum
成员变量:
static int _i
:静态成员变量,用于记录当前要累加的数字。初始化为 1。static int _ret
:静态成员变量,用于存储累加的结果。初始化为 0。构造函数:
- 每次创建
sum
类的对象时,构造函数被调用。在构造函数中,将当前的_i
值累加到_ret
中,然后将_i
的值自增。这样,每次创建对象就相当于进行一次累加操作。静态成员函数
_GetRet()
:
- 用于获取
_ret
的值,即累加的结果。类
solution
- 成员函数
_solution(int n)
:
- 这个函数接受一个整数
n
,目的是计算从 1 加到n
的和。- 通过一个循环创建
n
次sum
类的对象。每次创建对象时,sum
类的构造函数会自动进行累加操作。- 最后,通过
sum::_GetRet()
获取累加的结果,并将其赋值给ret
,然后打印和返回这个结果。学习之后:
这里我们可以看出来,变成内部类之后,我们成员函数都不需要了,直接访问私有变量
这里采取的是vs编译器,vs编译器是不支持可变长数组的,所以代码有点出入
#include<iostream> using namespace std; class sol { //内部类 class Sum { public: Sum() { _ret += _i; ++_i; } static int _i; static int _ret; }; public: static int _sol(int n) { for (size_t i = 0; i < n; i++) { Sum s; } //这里不需要成员函数了,直接可以访问私有变量 return Sum::_ret; } }; //这里需要注意一下,因为我们需要突破类域 int sol::Sum::_i = 1; int sol::Sum::_ret = 0; int main() { sol s; int ret = s._sol(100); cout << ret << endl; return 0; }
匿名对象(没有名字的对象,类似于临时对象)
概念概述
- 用类型(实参)定义出来的对象叫做匿名对象,相比之前我们定义的类型对象名(实参)定义出来的叫有名对象
- 匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象。
使用
代码
#include<iostream> using namespace std; class A { public: A() { cout << "A()构造" << endl; } ~A() { cout << "~A()析构" << endl << endl; } void _print() { cout << "测试匿名" << endl; } private: int _a; }; int main() { A(); //匿名 A()._print(); //延长声明周期(虽然使用了const A& r = A();试图延长临时对象的生命周期,但是仍然不能访问print函数,原因如下:临时对象的限制:编译器的限制) const A& r = A(); return 0; }
注意事项:
匿名对象可以延长生命周期,只需要进行绑定就可以进行延长生命周期
对象拷贝时的编译器的优化(就业方向:编译器国产化的开发)
对象拷贝时的编译器优化
- 现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少一些传参和传返回值的过程中可以省略的拷贝。
- 如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更"激进"的编译器还会进行跨行跨表达式的合并优化。
传值返回的编译器的优化
传值返回是会产生拷贝的
传值返回是不能返回已经销毁的对象,一般都是返回临时对象
赋值的优化