自动的小企业网站建设,无货电商怎么入门,外包员工和正式员工区别,wordpress怎么建栏目文章目录1. 再谈构造函数1.1 初始化列表1.2 explicit关键字2. static 成员2.1 静态成员变量2.1 静态成员函数2.3 练习2.4 总结3. 匿名对象4. 友元4.1 友元函数4.2 友元类5. 内部类6. 拷贝对象时编译器的一些优化7. 再次理解类和对象这篇文章呢#xff0c;我们来再来对类和对象…
文章目录1. 再谈构造函数1.1 初始化列表1.2 explicit关键字2. static 成员2.1 静态成员变量2.1 静态成员函数2.3 练习2.4 总结3. 匿名对象4. 友元4.1 友元函数4.2 友元类5. 内部类6. 拷贝对象时编译器的一些优化7. 再次理解类和对象这篇文章呢我们来再来对类和对象做一些补充进行一个最后的首尾 1. 再谈构造函数 那上一篇文章呢我们学了类的6个默认成员函数其中我们第一个学的就是构造函数。 那我们先来回忆一下构造函数 构造函数是一个特殊的成员函数名字与类名相同,创建类类型对象时由编译器自动调用以保证每个数据成员都有 一个合适的初始值并且在对象整个生命周期内只调用一次。 也就是说构造函数其实就是帮我们对类的成员变量赋一个初值。 举个栗子
class Date
{
public:Date(int year, int month, int day){_year year;_month month;_day day;}
private:int _year;int _month;int _day;
};对于像这样的一个类来说 虽然经过上述构造函数的调用之后对象中的成员变量已经有了一个初始值但是不能将其称为对对象中成员变量的初始化构造函数体中的语句只能将其称为赋值。 因为初始化只能初始化一次即在定义的时候赋初值而构造函数体内可以对成员变量进行多次赋值。 这里注意初始化定义的时候赋初值和赋值的区别。 那我们现在来看这样一个类
class A
{
private:int _a1;int _a2;
};问大家一个问题这里面的int _a1; int _a2; 是对成员变量_a1、 _a2的声明还是定义 这里是不是声明啊只是声明一下A这个类里有这样两个成员变量。 那它们在哪定义呢 是不是在这个时候 但是这里不是对对象整体的定义嘛。 那对象的每个成员变量什么时候定义呢 可是变量整体定义了的话它的成员不都也定义了吗 这些成员不都是属于这个对象的吗 我们运行也没出什么问题。 道理好像是这样的但是呢看这种情况 我们现在给这个类里面再增加一个const的成员变量。 那这时我们再去运行程序 哦豁发生错误了这么回事 为什么会这样呢 大家来想一下const修饰的变量有什么特点 const修饰的变量必须在定义的时候赋初值初始化 而我们现在有对_b进行初始化吗 是不是没有啊我们构造函数都没写那编译器是会默认生成一个但是我们知道默认生成的根本就不会对内置类型进行处理。 那我们是不是自己写个构造函数就行了 但是我们发现还不行为什么呢 因为const变量必须是在定义的时候赋初值而我们上面说了构造函数里面只是对其赋值并不是初始化。 那大家可能想到了 之前文章里我们在讲解构造函数的时候说了C11不是允许内置类型成员变量在类中声明的时候可以给缺省值嘛。 我们来试一下 这样确实不报错了。 但是这是C11之前才提出来的那C11之前呢 如何解决这样的问题呢 现在的问题是什么 _b必须初始化即在定义的时候赋初值但是现在是不是没法搞啊构造函数里只能对其赋值并不是初始化。 那我们是不是要给成员变量也找一个定义的位置不然像const这样的成员变量不好处理。 那成员变量的定义到底是在哪里呢 我们可以认为对象定义的时候其成员变量也就定义了但是一个对象可能有多个成员在对象定义的地方也没法给某个成员初始化啊。 怎么办 这就是我们接下来要学的东西——初始化列表。 1.1 初始化列表 那面对上面的问题我们的祖师爷就要去给成员变量找一个定义的地方那最终找来找去呢还是把目标锁定在了构造函数。 在构造函数里面呢又搞了一个东西叫做——初始化列表。 初始化列表 以一个冒号开始接着是一个以逗号分隔的数据成员列表每个成员变量后面跟一个放在括号中的初始值或表达式。 举个栗子 对于上面类中const int _b的初始化我们就可以放在初始化列表进行处理 class A
{
public:A(int a1, int a2, int b):_a1(a1),_a2(a2),_b(b){}
private:int _a1;int _a2;const int _b;
};int main()
{A a(1, 1, 1);return 0;
}这下我们再运行程序 就可以了。 当然 在构造函数体内我们还可以再为成员变量赋值 注意这里成员_b被const修饰不能再被赋值了。 然后呢对于初始化列表还有一些需要我们注意的地方 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次) 以下三种类成员变量必须放在初始化列表位置进行初始化 引用成员变量 const成员变量 没有默认构造函数的自定义类型成员
首先const成员变量 我们上面举的例子就是const成员变量它必须在定义的时候赋初值所以必须在初始化列表对其进行初始化定义的时候赋初值当然C11之后可以给缺省值这样如果没有对它进行初始化编译器就会用缺省值去初始化。 然后还有引用成员变量 这个我们在之前学习引用的时候就说了 引用也必须在定义的时候初始化。 最后就是没有默认构造函数的自定义类型成员 因为默认生成的构造函数对内置类型不做处理对自定义类型会去调用它对应的默认构造函数不需要传参的构造函数都是默认构造函数所以如果自定义类型成员没有默认构造函数我们就需要自己去初始化它。 举个栗子
class B
{
public:
private:int _b;
};
class A
{
private:int _a1;int _a2;B _bb;
};int main()
{A a;return 0;
}大家看运行这个程序有问题吗 没有问题因为对于成员B _bb;来说会调用它对应的默认构造类B我们虽然没写构造函数但是有编译器默认生成的构造函数。 当然如果我们写了不用传参的构造函数也可以。 但是如果这样 此时类B是不是没有默认构造函数了。 那这时就不行了。 让_bb在初始化列表调用其构造函数进行初始化这样就可以了。 尽量使用初始化列表初始化因为不管你是否使用初始化列表成员变量都会在初始化列表定义 成员变量在类中声明次序就是其在初始化列表中的初始化顺序与其在初始化列表中的先后次序无关
看这个程序
class A
{
public:A(int a):_a1(a), _a2(_a1){}void Print() {cout _a1 _a2 endl;}
private:int _a2;int _a1;
};
int main() {A aa(1);aa.Print();
}大家思考一下结果是啥 构造函数参数a接收传过来的11先初始化_a1然后_a1去初始化_a2所以都是1是吗 结果是1和一个随机值。 为什么是这样 原因就是成员变量在类中声明次序就是其在初始化列表中的初始化顺序与其在初始化列表中的先后次序无关 所以先初始化_a2然后是_a1 所以是1和随机值。 1.2 explicit关键字
我们先来看这样一个类
class A
{
public:A(int a):_a1(a){}private:int _a2;int _a1;
};那我们现在想用A这个类去创建对象
int main()
{A a1(1);return 0;
}这样肯定是可以的去调它带参的构造函数。 那除此之外呢其实还可以这样搞
欸这种写法是怎么回事 这个地方为什么A a2 1;这样也可以1是一个整型怎么可以直接去初始化一个类对象呢 那要告诉大家的是这里其实是一个隐式类型转换。 就跟我们之前提到的内置类型之间的隐式类型转换转化是一样的会产生一个临时变量我们再来回顾一下 那这里A a2 1是如何转换的呢 这里呢也会产生一个临时变量这个临时变量就是用1去构造出来的一个A类型的对象然后再用这个临时对象去拷贝构造我们的a2。 那我们可以来证明一下是不是如我所说的那样 我们来再写一个拷贝构造 注意拷贝构造也是有初始化列表的因为拷贝构造函数是构造函数的一个重载形式。 那我们现在运行程序看A a2 1是不是先用1调构造函数创建一个临时变量然后再调拷贝构造构造a2。 如果是那就跟我们上面说的一样了。 哦豁构造确实调了但是后面没去调拷贝构造啊。 是我们上面说的不对吗 那其实呢C编译器针对自定义类型这种产生临时变量的情况会进行优化 编译器看到你这里先拿1构造一个对象然后再去调拷贝构造有点太费事了干脆优化成一步直接拿1去构造我们要创建的对象。 当然不一定所有的编译器都会优化但是一般比较新一点的编译器在这里都会优化。 但是呢口说无凭欸 你凭什么说这里没有优化的话是会产生临时变量的说不定人家本来就是直接去构造了呢 那我们再来看这个代码 A c 10; 这样可以吗 不行直接报错了。 但是 加个const就行了。 为什么呢 这是不是我们之前在常引用那里讲过的啊 这里产生了临时变量而临时变量具有常性所以我们加了const就行了。 欸那不是说优化了嘛但是这里是引用就没优化了直接拿10去构造一个临时对象然后c就是这个临时对象的引用所以只有一步构造就不用优化了。 所以这里确实是会产生临时变量的上面那种情况确实是进行了优化。 还有一个点就是一般来说C 中的临时变量在表达式结束之后 (full expression) 就被会销毁而这里引用去引用一个临时变量的话会延长它的声明周期的。 那上面说了这么一大堆想告诉大家的是什么呢 就是 构造函数不仅可以构造与初始化对象对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数是支持隐式类型转换的C98就支持这种语法了。 这里就可以这样 那如果我们这里不想让它支持类型转换了有没有什么办法呢 这就要用到我们接下来要学的一个关键字——explicit 我们只需在对应得构造函数前面加上explicit关键字 然后 这样写就不行了。 但是呢我们刚才说的是对于单参数的构造函数是支持这种类型转换的那多参数的构造函数呢 首先我们肯定是可以这样用的 A a(1, 1); 那这里能不能也像上面那样支持隐式类型转换呢两个参数的构造函数那这样去用 那首先要告诉大家C98是不支持多参数的构造函数进行隐式类型转换的。 不过呢C11对这块进行了扩展使得多参数的构造函数也可以进行隐式类型转换但是要这样写 用一个大括号括起来。 那同样的道理如果我们不想让这里支持这种类型转换对于多参数的构造函数也是在前面加一个explicit关键字 用explicit修饰构造函数将会禁止构造函数的隐式转换。 2. static 成员
我们来看这样一个面试题 要求我们实现一个类并在程序中能够计算出一共创建了多少个类对象。 那大家想一下可以怎么做 直接去数行不行啊 直接数是不是有可能不靠谱啊因为有些创造类对象的地方是不是会进行优化这个我们上面刚刚讲过。 那还可以怎么搞呢 大家想一下要创建一个类对象有哪些途径是不是一定是通过构造函数或者拷贝构造搞出来的。 那我们是不是可以考虑利用构造函数和拷贝构造来计算创建出来的对象个数啊。 class A
{
public:A(int a){}A(const A aa){}
};假设我们现在创建了这样几个对象
void func(A a)
{}
int main()
{A a1;A a2(a1);func(a1);A a3 1;return 0;
}那我们就可以怎么做 定义一个全局变量n初值为0。 然后每次调用构造函数或者拷贝构造创建对象时就让n那这样最后n的值是不是就是创建的对象的个数啊。 我们来测试一下 结果是4对不对啊。 我们分析一下其实就是4答案没问题。 但是大家说当前这种方法好吗 其实是不太好的为什么 首先这里我们用了一个全局变量那首先第一个问题就是它可能会发生命名冲突其次全局的话是不是在哪都能访问它而C讲究封装都可以修改它啊如果在其它地方不小心了几次结果是不是就不准了啊。 那有没有更好一点的方法呢 那当然是有的。 应该怎么做呢 我们把统计个数的这个变量放到类里面这样它就属于这个类域了就不会命名冲突了然后如果不想让它在类外面被访问到我们可以把它修饰成私有的就行了。 但是 如果直接放到类里面作为类的一个成员变量 那它是不是就属于对象了但我们要统计程序中创建对象的个数这样我们每次创建一个对象n就会定义一次是不是不行啊。 不能让它属于每个对象是不是应该让它属于整个类啊。 2.1 静态成员变量
怎么做呢 在它前面加一个static修饰让它成为静态成员变量。 那这样它就不再属于某个具体对象了而是存储在静态区为所有类对象所共享。 但是我们发现加了static之后报错了为什么 因为静态成员变量是不能在这里声明的时候给缺省值的。 非静态成员变量才可以给缺省值。 大家可以想一下嘛缺省值其实是在什么时候用的在初始化列表用的用来初始化对象的成员变量的而静态成员变量我们说了它是属于整个类的被所有对象所共享。 类里面的是声明那静态成员变量的初始化应该在哪 规定静态成员变量的初始化一定要在类外定义时不添加static关键字类中只是声明。 但是现在又有一个问题 我们把它搞成私有的在外面就不能访问了。 当然如果不加private修饰就可以了 另外呢这里除了指定类域来访问静态成员变量还可以通过对象去访问 因为它属于整个类并且被所有对象共享。 还可以这样 这个问题我们之前是不是说过啊不能看到-或者.就认为一定存在解引用还是要根据具体情况进行分析。 当然如果是私有的情况下这样写是不是统统不行啊 那我们就可以写一个Get方法 成员函数是需要通过对象去调用的。 这样就可以了。 那如果我们的程序是这样的呢 在main函数里面我们根本没有创建对象那我们还怎么调用Getn函数呢 难道我们专门在main函数里创建一个对象去调用Getn然后再把结果减1 因为main函数里的对象是我们为了调用函数而创建的对象所以最后要减去。 2.1 静态成员函数
那有没有什么办法可以不通过对象就能调用到Getn函数呢 那我们就可以把Getn函数搞成静态成员函数也是在前面加一个static关键字就行了。 但是静态成员函数有一个特性静态成员函数没有隐藏的this指针不能访问任何非静态成员。 因为非静态成员是属于对象的都是通过this指针去访问的而静态成员函数是没有this指针的。 那它没有this指针就可以不通过对象调用了所以现在我们通过指定类域也可以调用到静态的Getn函数。 当然你还通过对象调用也还是可以的。 那我们现在在加一个东西 大家觉得现在结果会多几个对象 13个是不是比之前多了10个啊因为我们又多定义了一个大小为10 的类对象数组。 2.3 练习
那接下来我们来做个题: link 这道题呢就是让我们求一个1到n的和但是要求了一大堆不让用这不让用那的。 那不用就不用呗其实借助我们刚才学的知识就可以很巧妙的去解这道题。 怎么做呢 我们自己呢定义这样一个类两个静态成员变量_i和_sum分别初始化为01。 然后我们调用一次构造函数就让_sum_i然后_i这样第一次1第二次2… 那现在我们要求123…n的和怎么办 是不是是需要调用n次构造函数所以我们直接定义一个大小为n的类对象数组就行了。 class Sum{
public:Sum(){_sum _i;_i;}static int GetSum(){return _sum;}
private:static int _i;static int _sum;
};
int Sum::_i1;
int Sum::_sum0;class Solution {
public:int Sum_Solution(int n) {Sum arr[n];//C99支持变长数组可以用变量指定数组大小但不能初始化。return Sum::GetSum();}
};这样就行了。 2.4 总结
那最后我们来总结一下 声明为static的类成员称为类的静态成员用static修饰的成员变量称之为静态成员变量用static修饰的成员函数称之为静态成员函数。 特性 静态成员为所有类对象所共享不属于某个具体的对象存放在静态区静态成员变量必须在类外定义定义时不添加static关键字类中只是声明静态成员变量一定要在类外进行初始化类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问静态成员函数没有隐藏的this指针不能访问任何非静态成员静态成员也是类的成员受public、protected、private 访问限定符的限制 3. 匿名对象
接下来我们再来学一个知识叫做匿名对象什么是匿名对象呢 那现在呢有这样一个类 我们现在想要那这个类去创建对象那除了我们之前学的方法之外其实我们还可以这样创建对象 这里我们就拿A这个类创建了一个匿名对象。 匿名对象的特点就是没有名字但是它的声明周期只在创建它的这一行。 我们可以来证明一下 我们通过调式可以发现113行执行完这个匿名对象就已经调用了它的析构函数即它的声明周期已经结束了。 那匿名对象有什么用呢 既然创造出来就一定是有用的。 比如 现在有一个类Solution里面有一个非静态成员函数Sum_Solution我们知道想要调用类里面的非静态成员函数是需要通过对象去调用的。 那现在有了匿名对象我们就可以这样调用了 匿名对象在这样场景下就很好用当然还有一些其他使用场景这个我们以后遇到了再说。 4. 友元
友元分为友元函数和友元类。
4.1 友元函数
那友元函数我们在上一篇文章是不是就用到了 在上一篇文章我们实现的日期类中 我们尝试去重载operator然后发现没办法将operator重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。 但是实际使用中cout需要是第一个形参对象才能正常使用。 所以要将operator重载成全局函数。但又会导致类外没办法访问成员然后我们就使用友元解决了。operator同理。 友元函数使得定义在类外部的普通函数可以直接访问类的私有和受保护成员该函数不属于任何类但需要在类的内部声明声明时需要加friend关键字。 说明 友元函数可访问类的私有和保护成员但不是类的成员函数友元函数不能用const修饰 经过之前的学习我们知道const修饰的是啥 const修饰非静态成员函数实际修饰的是this指针而友元函数根本都不是类成员函数所以都没有this指针。友元函数可以在类里面的任何地方声明不受类访问限定符限制一个函数可以是多个类的友元函数友元函数的调用与普通函数的调用原理相同 4.2 友元类
接下来我们再来学习一下友元类 我们来看这样一个场景 class 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;
};现在有两个类在Date类中有一个成员变量是Time类的对象。 Time类中的成员变量都是私有的那在Date类中我们想访问Time类成员的私有成员变量是不行的。 那想解决这个问题除了去写Get和Set方法还可以这样解决 就是声明日期类为时间类的友元类这样在日期类中就可以直接访问Time类成员中的私有成员变量了。 然后呢友元类还有一些需要我们注意的地方
友元关系是单向的不具有交换性 比如上述Time类和Date类在Time类中声明Date类为其友元类那么可以在Date类中直接访问Time类的私有成员变量但想在Time类中访问Date类中私有的成员变量则不行。 友元关系不能传递 若C是B的友元 B是A的友元但不能说明C是A的友元。 友元关系不能继承这个在后面讲到继承的时候再给大家详细介绍
5. 内部类
我们再来看一个东西叫做内部类什么是内部类呢 如果一个类定义在另一个类的内部那么这个类就叫做内部类。 比如像这样
class A
{
private:int h;
public:class B {private:int b;};
};那大家先来思考一个问题类A的大小是多少 对于类A来说首先有一个整型成员变量然后还有一个成员是类BB里面也有一个整型成员变量。 那两个整型变量A的大小是不是个字节啊 我们来验证一下 欸结果是为什么呢里面不是还有一个类B嘛。 我们通过调式可以发现 拿A定义一个对象它的里面只有一个成员变量并没有类B。 所以呢 这里想告诉大家的是 内部类并不属于外部类它和对应的外部类是相互独立的只是受外部类类域的限制。 对于上面那个类来说我们想拿A中的内部类B去创建对象这样是不行的 因为B是在A这个类域里面的。 这样就可以了。 另外呢 内部类也是受访问限定符的限制的。 刚才类B在A中是Public修饰的所以我们指定类域之后就可以访问了但如果B被private修饰呢 就不行了。 然后还有就是 内部类天生就是其对应的外部类的友元类。 那这样的话在B中就可以直接访问A中的私有成员了。 注意内部类可以直接访问外部类中的static成员不需要通过外部类的对象/类名。 但是我们说了友元关系是单向的所以 外部类对内部类没有任何的访问权限。 6. 拷贝对象时编译器的一些优化 在有些拷贝对象的情况下C编译器会做一些优化减少对象的拷贝这个在有些场景下还是非常有用的。 那其实在上面我们已经提到过一种场景了 我们说这种场景会发生一个隐式类型转换先拿1去构造一个临时对象然后再拷贝构造给对象a。 但是呢编译器会进行一个优化直接拿1去构造对象a。 那除此之外在某些传参和传返回值的过程也会有这样的优化。 来看这个类
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;
};看这个场景 大家思考一下在调用fun1传参的过程中这里会发生优化吗 我们来分析一下这里正常的逻辑是先拿2去构造一个临时对象然后再去拷贝构造形参a。 那这里肯定也是会直接优化成一步构造的。 我们可以来验证一下 是不是只有一步构造啊这里析构的其实是fun1中的a。 还有这种情况也是同样的道理。 当然这几种情况如果我们传的是引用的话那就不用拷贝了所以传参能用引用的话可以尽量传引用。 再来看这种场景 main函数里面只调用了一下fun2在fun2函数里面先是一个构造然后是一个拷贝构造因为a出函数就销毁了返回的是一个临时变量是a的拷贝这个也是我们前面讲过的知识。 那这里会优化吗 不会的因为这里的构造和拷贝构造并不是一个表达式里的是分开的两步。当然不同的编译器也可能会不同处理。 我们可以调式观察一下 那我们如果写成这样呢 这里跟上面那样写相比是不是又多了一个拷贝构造啊。 返回值返回是一个拷贝构造然后紧接着又把返回值拷贝构造给了aa。 那这里会优化吗 是会的因为这两个拷贝构造是不是一个连续的过程啊。 可以看到这里跟上面是一样的。当然这是编译器优化的结果。 那如果我们接收返回值这样接收呢 我们先定义一个对象然后拿定义好的对象去接收返回值。 这两种写法有什么不同呢我们来对比一下 大家可以看一下差别还是挺大的。 第一种写法呢是构造函数内一个拷贝构造返回值优化之后是这样但是第二种的话是首先main函数内构造一个对象aa2然后函数内一个构造一个拷贝构造最后又赋值给aa2。 所以说 接收对象返回值的时候尽量用拷贝构造的方式接收不要赋值接收。 再来看这样呢 刚才我们是先构造一个对象然后返回那如果现在直接返回一个匿名对象呢 那这样的话匿名对象的构造和返回时的拷贝构造是不是就连续了所以这里编译器就会对它进行优化了 直接返回匿名对象的情况下构造拷贝构造就被优化成直接构造了。 所以在返回对象时能用匿名对象的话可以选择用匿名对象。 7. 再次理解类和对象 现实生活中的实体 计算机并不认识计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体用户必须通过某种面向对象的语言对实体进行描述然后通过编写程序创建对象后计算机才可以认识。 比如想要让计算机认识洗衣机就需要 用户先要对现实中洗衣机实体进行抽象——即在人为思想层面对洗衣机进行认识洗衣机有什么属性有那些功能即对洗衣机进行抽象认知的一个过程经过1之后在人的头脑中已经对洗衣机有了一个清醒的认识只不过此时计算机还不清楚想要让计算机识别人想象中的洗衣机就需要人通过某种面相对象的语言(比如C、Java、Python等)将洗衣机用类来进行描述并输入到计算机中经过2之后在计算机中就有了一个洗衣机类但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的通过洗衣机类可以实例化出一个个具体的洗衣机对象此时计算机才能知道洗衣机是什么东西。用户就可以借助计算机中洗衣机对象来模拟现实中的洗衣机实体了。 在类和对象阶段大家一定要体会到类是对某一类实体(对象)来进行描述的描述该对象具有哪些属性哪些方法描述完成后就形成了一种新的自定义类型用然后用该自定义类型就可以实例化具体的对象。 那我们这篇文章的内容就到这里欢迎大家指正