C++ - 类和对象 #取地址运算符重载 #构造函数 #类型转换 #友元 #static 成员 #内部类 #匿名对象 #对象拷贝时编译器的优化

文章目录

一、取地址运算符重载

1、const 成员函数

2、取地址运算符重载

二、构造函数的深入了解

1、知识点汇总:

2、详细理解:

3、小练习1:

三、类型转换

1、知识点汇总:

2、详细理解:

四、友元

1、知识点汇总:

2、详细理解:

五、static 成员

1、知识点汇总:

2、详细理解:

3、小练习1:

4、小练习2:

六、内部类

1、知识点汇总:

2、详细理解:

七、匿名对象

1、知识点汇总:

2、详细理解:

八、对象拷贝时的编译器的优化

1、知识点汇总:

2、详细理解:

场景一:构造+拷贝构造

场景二:在传参的过程之中也会有优化

场景三:传值返回

总结


前言

路漫漫其修远兮,吾将上下而求索;


一、取地址运算符重载

1、const 成员函数

const 修饰的成员函数称为const 成员函数,const 修饰成员函数放到成员函数参数列表的后面

const 实际修饰该成员函数隐含的this 指针,表明该成员函数中不能对类的成员任何成员变量进行修改

  • eg. const 修饰Date 类的Print成员函数,Print 隐含的this 指针由 Date* const this 变为 const Date* const this , 即放在成员函数后面的const 修饰的是this 指针指向的内容;

Q1:为什么存在修饰this 指针这样的概念呢?

  • 因为实践当中有的场景会涉及权限的放大;

例如:传的形参为const 对象,那么形参也得只用const 修饰,权限可以缩小、平移,但是不能放大;

调用成员函数需把对象的地址传递给this 指针。此处为日期类,故而此处的this 指针的类型为Date* (不加任何修饰,默认this 指针的类型为 Date* const this ,因为不可以更改this 的指向),若对象为const 对象,那么形参也得使用const 修饰,实现权限的平移

而又由于this 指针是隐式传递的,我们无法手动为this 指针指向的内容加const ,所以祖师爷在设计的时候,我们可以将const 放在成员函数参数列表后面以此来约定此const 修饰 *this;

Q:祖师爷为什么不默认写为const Date* const this ,而是写成 Date* const this?

  • 因为传过来的对象的指针,我们可能会修改this 指向的内容,可能不会修改,这是取决于需求的;如果默认为const Date* const this ,那么就写死了不能修改this 指向的内容;只有只读的接口成员函数才可以在其后加const 以修饰 *this , 此时实参可以传const 对象也可以传非const 对象(权限的平移、缩小);

需要注意的是,修饰this 指针本身的const 不涉及权限的放大、缩小、平移,因为修饰this 指针本身的const 代表这this 指针变量中存放的数据不可以修改;

从上图中可知,建议所有不修改成员变量的成员函数都建议加上const ;并且不必担心不加const 的函数就无法接收const 对象,因为不加const 的函数会改变this 指针指向的内容,故而只要在传参的时候不传const 修饰的对象即可;

2、取地址运算符重载

取地址运算符重载分为普通地址运算符重载const 取地址运算符重载一般这两个函数编译器自动生成的就够用了,无需我们显式实现除非在一些很特殊的场景中,比如我们不想让别人取到当前类对象的地址,此时我们就可以显式实现取地址运算符重载函数,随便返回一个地址

Q:为什么分为普通取地址运算符重载函数和const 取地址运算符重载函数?

(以类Date 为例子讲解:)

  • 因为给普通对象调用的取地址运算符重载函数和给const 调用的取地址运算符重载函数是不一样的,普通对象的类型为Date * ,而const 对象的类型为const Date* ,取得的地址的类型也不同,所以分为普通对象和const 对象;

参考代码:

	//普通对象取地址运算符重载函数
	Date* operator&()
	{
		return this;
	}
	
	//const 对象取地址运算符重载函数
	const Date* operator&() const
	{
		return this;
	}

当这两种写法,如果只有一个(只存在const 对象的取地址运算符重载函数)它也是可以匹配的,但倘若同时存在,它会去调用最匹配的;

需要注意的是,默认的取地址运算符重载函数有两个,但我们显式实现其中一个的时候,另外一个编译器就不会自动实现了;

我们在显式实现的const对象取地址重载函数中增加一条打印的语句

例子如下:

实践当中我们不会显式实现,编译器默认生成的就已经够用了;但是倘若你不想让别人拿到地址,我们就可以显式实现,随便返回一个地址,代码如下:

	//普通对象取地址运算符重载函数
	Date* operator&()
	{
		return nullptr;
	}

	//const 对象取地址运算符重载函数
	const Date* operator&() const
	{
		return nullptr;
	}

测试一下:

二、构造函数的深入了解

1、知识点汇总:

  • 之前我们实现构造函数的时候,初始化成员变量主要使用函数体内赋值,构造函数初始化还有一种方式,就是初始化列表初始化列表的使用方式以一个冒号开始,接着是以逗号分割的数据或成员列表,每个“成员变量”后面跟一个放在括号中的初始值或者表达式
  • 每个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方;
  • 引用成员变量,const 成员变量,没有默认构造的类类型变量必须放在初始化列表位置进行初始化,否则就会编译报错;
  • C++11支持在成员变量声明的位置给缺省值,这个缺省值的目的主要是给没有显式在初始化列表的成员使用的
  • 尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员变量也会走初始化列表如果这个成员在声明的位置给了缺省值,初始化列表就会用这个缺省值进行初始化;如果你没有给初始值,对于没有显式在初始化列表中初始化的内置类型成员变量是否初始化取决于编译器,C++对于这个行为并没有规定;对于没有在初始化列表中初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造函数编译器就会报错;
  • 初始化列表中按照成员变量在类中声明的顺序进行初始化,跟成员变量在初始化列表中出现的先后顺序无关。建议成员变量的声明顺序和初始化顺序保持一致;

2、详细理解:

回忆前面写的有关构造函数的博文,MyQueue 其中的成员变量均是自定义类型,当MyQueue实例化对象的时候,编译器会自动调用栈Stack 的默认构造函数;(注:默认构造函数包括:无参、全缺省、没有显式实现编译器自动生成的构造函数,其特点就是不用传参便可以调用);但是倘若我们创建MyQueue的时候知道会放进去多少个数据,那么此时当栈Stack 在构造的时候就不期望使用默认构造中默认开辟4个数据空间,这个时候该怎么做?

  • 不让MyQueue对象实例化的时候让编译器取调用栈Stack 的默认构造函数(实际上栈Stack 的默认构造函数为全缺省构造函数),那么我们就得自己为MyQueue提供显式的构造函数,如何初始化pushst 和 popst 这两个栈呢?利用初始化列表

Q:什么是初始化列表?

  • 构造函数其函数体中间加的东西就叫做初始化列表
class Date
{
public:

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

	}
private:
	int _year;
	int _month;
	int _day;
};

函数体必须写,这是函数的语法规则;但是函数体中可以没有内容;以在初始化列表中初始化一部分也可以在函数体中初始化一部分;

Q:为什么初始化列表不在构造函数的函数体中?

  • 一个对象的成员变量的定义严格来说来说只能出现一次,倘若将初始化列表放入函数体中也就不能保证其成员变量只初始化了一次,因为函数体中可能会存在复杂的逻辑,例如循环等,而导致重复初始化;

解决开头提出的问题,我们可以利用初始化列表显式实现MyQueue 的构造函数,如下:

typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}
		_capacity = n;
		_top = 0;
	}
private:
	STDataType* _a;
	int _capacity;
	int _top;
};

class MyQueue
{
public:
	MyQueue(int n)
		:_pushst(n)
		,_popst(n)
	{

	}
private:
	Stack _pushst;
	Stack _popst;
};

测试:

通过初始化列表可以解决默认构造函数对于自定义类类型的成员变量只会取调用该自定义类类型的成员函数的默认构造函数;

关于MyQueue需要使用初始化列表的两个场景:

  • 场景一:MyQueue 要生成默认构造函数的前提是编译器可以调用MyQueue自定义类型成员变量中的默认构造函数;倘若Stack 中没有默认构造函数又该怎么办?
  • 场景二:即使Stack 类中有默认构造函数,但是在MyQueue中不想让编译器调用Stack 类中的默认构造函数去初始化,想要自己显式地去传递一个值,自己去控制大小,此时Stack 类中的默认构造函数就不符合要求;

这两个场景,简单来说就是Stack 中没有默认构造函数,或者Stack中有默认构造函数但是不符合我们的要求,此时就需要我们利用初始化列表初始化显式实现MyQueue的默认构造函数

在MyQueue实例化对象的时候,还需要去调用构造函数去初始化其成员变量;当其成员变量为自定义类型且调用它自己的默认构造,或者没有默认构造函数、或者其成员变量为引用成员变量、const 成员变量;此时就要用到初始化列表来解决;

Q: 没有默认构造函数、或者其成员变量为引用成员变量、const 成员变量 这三类是必须使用初始化列表进行初始化的,为什么?

  • 一个自定义类类型中没有默认构造函数,就意味着需要传参去调用构造函数;(默认构造函数就是不用传参编译器自动调用的函数,那么没有默认构造函数就意味着需要我们自己传参去调用的函数);引用的语法规定,引用必须在定义的时候初始化const 也必须在定义的时候初始化,因为const 修饰的变量只有一次修改(初始化)它的机会,就是在定义的时候;变量一旦被const 修饰之后就不可以再修改了;即这三类成员变量的初始化必须在定义的时候就初始化;

每个成员变量都要找一个定义的位子,而有些成员变量必须在定义的时候初始化

Q:如果是不属于这三种情况的成员变量是否意味着不能在初始化列表中初始化呢

是否必须在初始化列表初始化的核心:该成员变量是否必须在定义的时候就要初始化;

所有的成员变量均可以在初始化列表中初始化,但是只能出现一次,如下图所示:

如上图,初始化列表和函数体可以混着用,对于非上述提到的三类成员变量来说可以这么做,因为这些成员变量可以不在定义的时候初始化;而这三类:自定义成员变量但是没有默认构造函数、引用、const 对象就必须在定义的时候初始化,而它们定义的地方在初始化列表;

尽管如此,但还是建议尽量使用初始化列表进行初始化,因为无论该成员变量是否在初始化列表中初始化了,最终均会走初始化列表,即只要该成员变量声明了,就算你没有在初始化列表中初始化该成员变量,也会在初始化列表中走一遍;需要注意的是,未在初始化列表中定义该成员变量,编译器会为该成员变量给随机值或者0,这个行为是没有定义的,取决于编译器;

Q:为什么一个类的所有成员变量均会走初始化列表?

因为初始化列表是这些成员变量定义的地方,故而在初始化列表中所有成员变量均会走一遍;整体的空间是该对象定义(实例化)的时候就已经开了,不要理解为,初始化列表在开辟空间;

初始化列表是包含在构造函数内部的,对象实例化的时候便会去自动调用构造函数;

Q:如果MyQueue 没有默认构造,除了用初始化列表还能有什么方法让其生成默认构造?如下例:

类Stack 中的构造函数并不是默认构造函数,而MyQueue没有默认构造的原因:MyQueue的成员变量是自定义类Stack,而Stack 这个类当中的构造函数是需要传参的,所以MyQueue 的构造就需要调用Stack 的构造并为之传参;除了用初始化列表还可以在成员变量声明的地方给缺省值,因为不管我们有没有将这个成员变量写在初始化列表中初始化,这个成员变量均是会去走初始化列表的,如下:

在声明处给缺省值,这个缺省值实际上是给初始化列表的;因为成员变量均会走初始化列表,那么在定义的时候,就用这些缺省值初始化;

Q:能否将直接在初始化列表初始化与成员变量声明时给缺省值混合使用?

  • 可以的,如下:

需要把握缺省值的思想显式 给了所要赋值(初始化)的数据,便不会使用该缺省值;同样地,如果在初始化列表中给了要初始化的值就不会去使用声明当中给的缺省值,如下:

需要注意的是,虽然有初始化列表,但是有时候初始化列表也只能初始化一部分功能,还需要借助于函数体去完善,eg:初始化一个数组,如下:

前文也多次提过:不管你有没有在初始化列表中初始化该成员变量,每个成员变量均会去走初始化列表;但是倘若走完了初始化列表,但是功能未完成就会在函数体中继续实现

但是,尽量使用初始化列表来初始化或者在声明的时候给缺省值,然后进一步检查或处理,决定是否要继续使用函数体处理;需要注意的是,初始化的时候要么选择全部使用初始化列表初始化,要么全部给缺省值,尽量不要两个混合着使用;

同样的,数组的初始化,也可以给缺省值,如下:

还有一点需要注意,在声明的时候给缺省值不可以用  () 给缺省值,可以用 = , {} 以及 ={} ,如下:

不可以用  () 给缺省值:

可以用 = 给缺省值:

可以用 {} 给缺省值:

可以用 ={} 给缺省值:

还有一点就是,初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的先后顺序无关。建议声明的顺序与初始化列表中的顺序保持一致;

3、小练习1:

我们先来做一道题:

答案:D

分析:首先,类A中的构造函数不是默认构造需要传参,其次类A中的两个成员变量均写在初始化列表中进行初始化;而由于先声明_a2 ,然后再声明_a1所以先走 _a2(_a1) ,然后再走_a1(a);初始化_a2 的时候,_a1 中为随机值,所以选D;图解如下:

运行如下:

从这道题,可以显然地知道,初始化列表初始化的顺序并不是按照初始化列表中我们写的顺序,而是按照成员变量声明的顺序初始化的;可以理解为走一遍内存当中的存储的顺序进行初始化;因为声明的顺序是对该对象成员变量在内存中存放的顺序,所以本质来说成员变量初始化的顺序是按照其在内存之中所占空间的先后顺序来进行初始化的,调试一下:

本质上就是按照内存中成员变量所占据空间的先后顺序进行初始化的,而内存之中成员变量的存放顺序取决于类成员变量声明时的顺序;

需要注意的是,不要将初始化列表的初始化定义为开辟空间,因为在对象实例化的时候,整体便已经开辟好了空间初始化列表只不过是为了初始化罢了

Q:如何理解”对象实例化的时候,整体便已经开辟好了空间“?

例如:

在一个函数之中,变量会在定义的地方开辟空间,只是说这个变量它会开辟空间,但并不是当执行到这一条语句的时候才开辟空间;变量i 的空间位于当前函数main的栈帧之中,栈帧建立的时候其空间其实就已经开辟好了;同理对于上面所提到的A类对象aa ,其空间就是其所在函数栈帧建立的时候就开辟好了的,如下:

只不过对于对象aa来说,A aa(1); 这一行是定义,需要初始化,而初始化又会调用构造函数,而在构造函数之中又给每个成员变量找了一个定义初始化的地方——初始化列表

Q:那么声明的时候成员变量的缺省值与构造函数的缺省值呢?

  • 初始化列表包含在构造函数之中构造函数的缺省值起作用是要有成员变量使用这个缺省值;(在初始化列表之中显式初始化并使用该形参,因为会先走初始化列表;),如下:

总结:

不管你是否在成员列表中显式地写该成员变量让其在初始化列表中初始化,每个成员变量均会走初始化列表倘若该成员变量并未在初始化列表中显式地进行初始化,如果该成员变量其声明的位置有缺省值便用缺省值,若无缺省值,就取决于这个成员变量是内置类型还是自定义类型;如果是内置类型就取决于编译器是初始化为随机值还是0,如果是自定义类型就看其是否有默认构造,如果没有默认构造函数就会报错;

需要注意的是,对于引用成员变量、const 成员变量、没有默认构造函数的成员变量 必须在初始化列表中初始化的,要么显式初始化要么给缺省值初始化

三、类型转换

1、知识点汇总:

  • C++支持内置类型隐式类型转换成类类型对象,需要有相关内置类型为参数的构造函数
  • 构造函数前面加explicit 就不再支持隐式类型转换,但是仍然支持显式类型转换;
  • 类类型的对象之间也可以隐式类型转换,需要相应的构造函数支持;

2、详细理解:

单参数构造函数和多参数构造函数均支持隐式类型转换,而隐式类型转换中间会产生临时变量;当然也支持显式类型转换,显式类型转换中间也会产生临时变量;倘若不期望隐式类型转换的话,可以在构造函数前面增加一个关键字:explicit ,但explicit 支持显式类型转换;

Q:内置类型的类型转换?

eg.

  • 首先会依据 i 生成一个浮点数 d 类型(double)的临时对象;只不过对于内置类型的类型转换时,语言默认解释支持的,编译器直接就处理了,支持内置类型之间有一定关联的类型的转换:例如:整形之间,整形与浮点数之间、整形和指针之间、指针和指针之间……

Q:为什么需要用构造函数去支持隐式类型转换呢?

  • 因为需要构造函数去支持类型转换创建的临时对象;

关于关键字explicit ,在该类的构造函数前面增加一个explicit 修饰,那么该类就不再支持隐式类型转换;

加上explicit之后:

之所以支持 A aa3  = 1; 这种写法,本质上就是类型转换;

Q:是如何通过构造函数实现的类型转换?

  • 类型转换会产生临时变量;先用1生成一个A类的临时对象,然后再用这个临时对象去拷贝构造对象aa3,如下图:

实际上,当编译器遇到连续的构造+拷贝构造的时候会优化成直接的构造;并且,并不是所有的编译器均会进行优化,优化与否取决于编译器;

Q:引用能否进行隐式类型转换?

  • 不能;

此处不能使用引用的原因并不是类型不同,而是权限的问题;常量1 赋值给A类对象,由于类型不同首先进行类型转换,而类型转换会产生临时变量,临时变量具有常性,倘若此处直接给A& ,就会导致权限的放大;

解决:加上一个const 进行修饰:

Q:多参数的构造函数能否支持隐式类型转换?

  • 可以的;

是直接使用 () 吗?

此处输出为2 ,1 就说明 _a1 为2 ,_a2 为1,这是因为 (2,2) 为逗号表达式,而逗号表达式的返回值为最后一个表达式所以 (2,2) 的返回值为2隐式类型的转换的时候还是调用的单参数的构造函数;故而此处本质上还是单参数的类型转换

想要隐式类型转换去调用多参数的构造函数应该使用 {} , 并且当使用引用的受需要加上 const 修饰:

A类实现的代码:

使用例子如下:

Q:类型转换有什么意义?

  • 如果你定义了一个类Stack ,想要往其中Push 自定义类型的数据,每次均需要创建该自定义类型的对象然后再Push ,这是非常麻烦的,如下:

但倘若有了隐式类型转换,就可以这么写,如下:

同样地,此处如果给A类的构造函数前加上关键字 explicit ,那么就不再支持隐式类型转换;但仍然支持显式转换,如下图:

此处的类型转换本质上就是将内置类型的对象转换为自定义类型的对象,C语言支持的内置类型的转换:(有一定关联的类型才能进行转换)

1、整形与浮点数之间(整形与浮点数均是用来表示数据大小)

2、整形和指针之间(指针是地址的编号,实际上也是一种大小)

3、整形和整形

4、指针和指针

5、有符号与无符号

而内置类型想要转换成自定义类型是需要借助于构造函数

Q:自定义类型之间可以转换吗?

  • 可以的,同理也需要对应的构造来支持;

需要用A类型对象aa1 生成一个B类型的临时对象,借助于一个形参A类型的B类的构造便可以实现

其实编译器会优化:,我们理解这条语句:先用aa1 构造一个B类型的临时对象,然后再将这个B类型的临时对象拷贝构造给bb1 ;即构造+拷贝构造,编译器会将其直接优化为构造:直接用aa1 构造对象bb1;(编译器优化这个问题,下文会详细讲解,此处稍做了解即可)

同理,此处倘若要用引用,也是需要加上const 的,因为临时对象具有常性,如下:

只需要加上const 即可:

四、友元

1、知识点汇总:

  • 1、友元是C++ 中提供的一种突破类访问限定符封装的方式友元分为:友元函数和友元类在函数声明或者类声明前面加上friend ,并且把友元声明放到一个类里面
  • 2、外部友元函数可以访问该类的私有、保护的成员,友元函数仅仅是一种声明,它并不是这个类中的成员函数
  • 3、友元函数可以在类定义的任何地方声明,并且不受访问限定符的限制
  • 4、一个函数可以是多个类的友元函数
  • 5、友元类中的成员函数可以是另一个类的友元函数(可以这么做,但是我们一般不会这么做,因为处理起来非常麻烦),都可以访问另一个类中的私有和保护成员
  • 6、友元类中的关系是单向的,不具有交换性,比如A类是B类的友元,但并不意味着B类也是A类的友元
  • 7、友元类的关系不可以传递,如果A类是B类的友元,B类是C类的友元,不能得到A类是C类的友元 
  • 8、友元有的时候可以提供便利,但是友元会增加耦合度,破坏了封装,所以友元不宜多用;

2、详细理解:

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

例如函数func 既要访问类A的私有,又要访问类B的私有,便可以将func 设置为类A与类B的友元函数

A类、B类的实现代码如下:

//需要增加一个前置声明,不然编译器看到A类中的声明并不知道B类是什么
class B;

class A
{
	//友元声明
	friend void func(const A& a, const B& b);
public:
	//explicit 
	 A(int a1)
		:_a1(a1)
	{}

	//explicit 
	 A(int a1 , int a2)
		:_a1(a1)
		,_a2(a2)
	{}

	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}

	int Get()const
	{
		return _a1 + _a2;
	}
private:
	int _a2 = 1;
	int _a1 = 2;
};

class B
{
	//友元声明
	friend void func(const A& a, const B& b);
public:

private:
	int _b1 = 0;
};

void func(const A& a, const B& b)
{
	cout << a._a1 << endl;
	cout << b._b1 << endl;
}

测试:

除了可以存在友元函数,还存在友元类

如果需要在类B中大量访问类A中的数据,此时如果一个函数一个函数地定义友元就会十分不方便,故而可以将类B设置为类A的友元类;

注:我们一般不会把一个类中的成员函数定义为另一个类的友元,可以这么做,但是处理起来十分麻烦;

A类、B类的实现代码如下:

class A
{
	//友元声明
	//friend void func(const A& a, const B& b);
	//友元类
	friend class B;
public:
	//explicit 
	 A(int a1)
		:_a1(a1)
	{}

	//explicit 
	 A(int a1 , int a2)
		:_a1(a1)
		,_a2(a2)
	{}

	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}

	int Get()const
	{
		return _a1 + _a2;
	}
private:
	int _a2 = 1;
	int _a1 = 2;
};

class B
{
	//友元声明
	//friend void func(const A& a, const B& b);
public:

	void func1(const A& a)
	{
		cout << a._a1 << endl;
		cout << _b1 << endl;
	}

	void func2(const A& a)
	{
		cout << a._a2 << endl;
		cout << _b2 << endl;
	}

private:
	int _b1 = 0;
	int _b2 = 1;
};

测试:

类B是类A的友元,那么类B就可以访问A中的所有成员,但是并不意味着类A是类B的友元,也并不意味着类A可以方位类B的所有成员;

五、static 成员

1、知识点汇总:

  • static 修饰的成员变量,称之为静态成员变量,静态成员变量一定要在类外进行初始化;
  • 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象之中,存放在静态区
  • 用static 修饰的成员函数,称之为静态成员函数静态成员函数没有this 指针
  • 静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this 指针;
  • 非静态的成员函数,可以访问任意的静态成员变量和静态成员函数;
  • 突破类域就可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数;
  • 静态成员也是类的成员受public、protected、private 访问限定符的限制
  • 静态成员变量不能在声明的位置给缺省值初始化,因为缺省值是在构造函数初始化列表中使用的,静态成员变量不属于某个对象,不走构造函数初始化列表

2、详细理解:

在C语言中,static 的作用:

  • 1、修饰变量可以修改该变量的生命周期;例如,一个局部变量属于当前栈帧,此时用static 修饰之后,便会变成一个静态变量,存储在静态区,即在程序运行期间存在
  • 2、static 修饰函数改变了该函数的链接属性将其外部链接属性改为了内部链接属性;例如,如果在多个 .h 之中有一个函数,那么这些.h 文件同时被包含便会出现链接错误,但是.h 中都加个static 修饰这个函数,就不会出现链接错误,这是因为static 修饰函数改变了这个函数的外部链接属性;让其只在当前文件可见,即多个.h 文件被同时包含之后是不同的变量;

同理,在C++ 中,也可以使用static 来修饰类之中的成员变量、成员函数;

当static 修饰成员变量之后就可以变成静态成员变量,即改变了这个成员变量的生命周期和存储的位置,这个成员变量就不属于该对象,并不能说该静态成员变量不属于这个类,类中写的均为成员变量的声明,当成员变量被static 修饰的以后,该类的对象实例化以后,这个被static 修饰的成员变量存储在静态区而非在该实例化的对象的空间中,可以认为这个成员变量为所有该类创建的对象所共享;

Q:为什么普通的成员变量可以在声明处给缺省值?

  • 在C++11 中支持普通成员变量在声明的时候给缺省值,而这个缺省值是给初始化列表初始化的时候使用的;

而初始化列表是这个对象在实例化的时候,编译器会自动地去调用其对应的构造函数去初始化;初始化列表被C++11认定为是每一个属于类的成员变量初始化的地方,如果该类的成员变量没有显式地在初始化列表中初始化便会用在声明时给的缺省值进行初始化;

静态成员变量并不会走初始化列表,因为该静态成员并不会属于某个对象

静态成员变量的使用方法在类中声明,在类外定义初始化;(不初始化也行,不初始化的话便会为随机值,这行为取决于编译器),如下:

Q:成员变量不是私有的吗?为什么此处可以在类外对其进行定义初始化?

  • 本质上相当于是声明与定义的分离,与之前写的日期类的声明与定义分离相同;

Q:含有静态成员的类有多大呢?

因为静态成员变量存放在静态区,除去_scount ,A类中就没有其他的成员变量了,而空类至少要占1byte 的空间,所以A类的大小为1byte;

Q:静态成员变量可以拿来做什么?

  • 静态成员变量属于这个类实例化的所有的对象;相当于是这个类的“全局变量”,那么就可以充当“全局变量”的功能;

而想要访问公有的静态成员变量,只要突破类域就可以访问;即让编译器在编译的时候知道要到哪里去寻找;此处突破类域的方式有两种,一种是 类名::静态成员变量,另一种是对象.静态成员变量,如下:

Q:倘若该静态成员变量不属于公有的呢?

  • 静态成员变量也还是类的成员,要受public、private 、private 访问限定符的限制;

如何访问私有的静态成员变量呢?

  • 利用静态成员函数:将static 放在成员函数的返回类型前面即可;写一个获取成员变量的静态Get,如下:

相较于普通成员函数,静态成员函数没有this指针,也就是说静态成员函数无法访问非静态成员变量、成员函数,因为想要访问这些成员本质上都是通过this 指针来进行的;即静态成员函数只能访问静态成员变量、静态成员函数;

  Q:倘若我想计算一个类实例化出了多少个对象,该如何做呢?

  • 可以将静态成员变量(生命周期为全局)当作一个计数器放在构造函数、拷贝构造函数中;(同时在析构函数中放-- ,就是在统计当前对象的个数),如下:

代码如下:

class A
{
public:
	A()
	{
		++_scount;
	}
	A(const A& a)
	{
		++_scount;
	}
	~A()
	{
		//--_scount;
	}
	static int GetACount()
	{
		return _scount;
	}
private:
	//类里面声明
	static int _scount;
};

//类外初始化
int A::_scount = 0;

int main()
{
	//通过指定类域进行访问
	cout << A::GetACount() << endl;
	A aa1, aa2;
	A aa3(aa1);
	//通过对象进行访问
	cout << A::GetACount() << endl;
	cout << aa1.GetACount() << endl;
	return 0;
}

测试如下:

3、小练习1:

求1+2+3+...+n_牛客题霸_牛客网

思路:

创建一个对象便会调用一个构造函数,倘若创建了n个对象,便会有n次调用构造函数的机会(构造、拷贝构造都属于构造);

而又如何一次便创建n个对象呢?利用变长数组(一般在OJ之中均会支持变长数组,因为OJ一般会使用较新、开放的编译器,例如牛客使用的是g++编译器、也有一些使用 clang++ 编译器);还需要注意的是,变长数组不可以初始化

Q: 那么又如何利用构造函数来实现呢?

  • 可以将静态成员变量放在构造函数中来计数;倘若想要完成追加就不能对普通变量累加,因为n个对象调用n次构造函数,每次所要加的数是不同的;那么就要使用全局对象来记录;而当我们定义为全局对象的时,别人能随意地修改,代码地健壮性就不是很好,故而就想到使用静态成员变量;

因为C++讲究封装,某种程度上来说,静态成员变量就是把一个全局变量封装在一个类之中,受改访问限定符的限制,如果再将其弄成私有的,那么便会称为改类专用的一个全局变量

此思想的核心:定义对象均会调用构造函数进行初始化;

参考代码:

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 {
public:
    int Sum_Solution(int n) 
    {
        //变长数组
        Sum arr[n];
        return Sum::GetRet();
    }
};

分析:

完全就可以将静态成员变量当作全局的,只不过静态成员变量被封装到类之中,会受到类域、访问限定符的限制;本质上就是创建出了一个仅该类使用的全局变量,而静态成员函数的作用就是访问该类中的私有的静态成员函数

实际上也可以使用new 来代替变长数组,代码如下:

参考代码2:

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 {
public:
    int Sum_Solution(int n) 
    {
        //变长数组
        //Sum arr[n];
        //使用new
        Sum* a = new Sum[n];
        delete[] a;
        return Sum::GetRet();
    }
};

Q:new 是什么?

  • new 会去堆上动态开辟一个数组,并且自动调用构造函数进行初始化;

4、小练习2:

答案为:E B

在进入main 函数之前,全局变量就要进行初始化;

Q:d 是一个静态的对象,静态的对象是什么时候进行初始化的呢?是否也是在main 函数之前初始化的呢?

  • 静态的变量虽然它的生命周期是全局的,但它并不是在main 函数之前初始化的,只有走到当前代码行才回去初始化;但倘若静态变量在一个函数之中,eg.在func 中;如果main 函数会调用func 函数两次,那么在调用第一次func 的时候,该静态变量会初始化,而在第二次调用的时候便不会再初始化了,因为静态变量只会初始化一次

所以调用构造函数的先后顺序为:C A B D;

先定义的后析构,后定义的先析构对象c 在全局,当main 函数结束时,该程序结束时才会去调用析构函数,那么对象c 最后一个析构;而同在main 函数的局部变量和静态变量,谁先析构呢?静态变量的生命周期时全局的,所以局部变量b、a 先析构,然后才是d;即调用析构函数的顺序是: B A D C;

六、内部类

1、知识点汇总:

  • 1、如果一个类定义在另外一个类的内部,这个在另外一个类里面的类就叫做内部类。内部类是一个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符的限制,所以外部类定义的对象中不包含内部类
  • 2、内部类默认是外部类的友元类, 即内部类可以访问外部类的所有成员
  • 3、内部类本质也是一种封装,当A类与B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A设计为B的内部类,如果放到private/protected 位置,那么A类就是B类的专属内部类,其他地方都用不了;

2、详细理解:

内部类就是把一个类定义到另外一个类里面(嵌套定义),里面这个类便叫做内部类;内部类是一个独立的类,跟定义在全局的类相比,它是一定会受到外部类类域与访问限定符的限制,所以外部类定义的对象不包含内部类;

注:我们可以简单地了解一下内部类,因为在实践中C++并不爱使用内部类;

代码如下:

//内部类
class A
{
private:
	//类里面声明
	static int _k;
	int _h = 1;

public:
	class B
	{
	public:
		void foo(const A& a)
		{
			cout << _k << endl;
			cout << a._h << endl;
		}

		int _b1 = 1;
		int _b2 = 1;
	};
};

//静态成员变量在全局进行定义
int A::_k = 1;

int main()
{
	cout << sizeof(A) << endl;

	return 0;
}

运行如下:

也就是说外部类A在实例化对象的时候并不会包含内部类B;内部类B仍然是一个独立的类,并不会作为外部类的一部分;只不过内部类会受到外部类类域和访问限定符的限制,即在全局的时候不能直接访问内部类,需要指定类域

内部类默认是外部类的友元,即内部类可以访问外部类的成员;

关于内部类使用例子,上面的练习1 我们还可以用内部类进行改造:

class Solution {
    //内部类Sum ,可以使用外部类Solution的成员
    class Sum
    {
    public:
        Sum()
        {
            _ret+=_i;
            ++_i;
        }
    };
public:
    int Sum_Solution(int n) 
    {
        //变长数组
        //Sum arr[n];
        //使用new
        Sum* a = new Sum[n];
        delete[] a;
        return _ret;
    }
private:
    static int _i;
    static int _ret;
};

//初始化
int Solution::_i = 1;
int Solution::_ret = 0;

Solution 这个类要实现 1+2+3+……+n ,并且不能使用乘除、循环、三目等,而类Sum 是用来专门计算的;类Solution 与类Sum 之间有紧密的联系,两个均在全局那么就可以实现,但是从封装的角度来说,便会有一个缺点但凡使用的人再用Sum 去定义一个对象,便会多调用一次构造函数而破环Sum 的计算

那么就可以考虑将Sum定义为 Solution 的内部类,并且弄成私有的;即限制了别人不能随意使用Sum, 类Sum 只有类Solution 才可以使用;

并且由于内部类Sum 是外部类Solution 的友元类,故而可以将Sum 的静态成员变量放为Solution 的成员变量;放在外部内就意味着内部类和外部类均可以使用;

内部类是一种更加紧密的合作关系,如果两个类分开,也可以完成相应的任务,两个类不存在谁控制谁、限制谁的关系;但是当Sum 放在类 Solution 内部且设为私有时候,Solution 限制了别人使用Sum ,并且完成了更好的封装;(内部类是外部类的友元类);

七、匿名对象

1、知识点汇总:

  • 1、用类型(实参)定义出来的对象叫作匿名对象,相比之前我们定义的类型+对象名(实参)定义出来的就是有名对象
  • 2、匿名对象的生命周期只在当前一行,如果定义一个对象只当前用一下即可,那么完全就可以直接定义成匿名对象来使用;

2、详细理解:

我们平时用类型定义出来的对象叫做有名对象,那么简单来说,没有对象名那么就是匿名对象

类的实现如下:

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}

private:
	int _a;
};

测试如下:

如果不想给有名对象传参,在创建的时候不传就行了,但是不能写作:A aa1(); 因为编译器无法识别:A aa1();究竟是一个函数申明还是对象定义;

但是我们可以这样定义匿名对象:A();  A(1);  因为匿名对象的特点就是不用取变量名;但是匿名对象的生命周期只有当前其一行;从上面运行结果可以大致知道:匿名对象在当前行创建,当前行结束就会自动去调用析构函数;

 Q:肯定是可以用引用引用有名对象的,但如果用引用来引用匿名对象呢?

  • 我们不可以直接用引用去引用匿名对象,因为匿名对象的生命周期只有一行,且匿名对象与临时对象有一样的效果,二者均具有常性;所以引用匿名对象需要加上const ,权限不能放大,权限只能平移与缩小;

注:需要注意区分中间的临时变量和匿名对象;匿名对象是我们主动创建的而临时变量是编译器因为一些语法需求而产生的,例如:传值返回、类型转换;匿名对象与临时对象之间有相似性,但是又存在不同;

利用加const 的引用引用了匿名对象,会延长这个匿名对象的生命周期;即这个匿名对象的的生命周期跟着引用走,即 ret1 销毁的时候,该匿名对象才会销毁; 

Q:匿名对象有什么用?

  • 匿名对象的使用场景主要有两个;

场景一:

倘若你想调用类Solution 中的Sum_Solution 方法该怎么做?

方法一:定义一个有名对象,通过点操作符去调用;

但是这样做的话,每一次调用该方法均要创建一个有名对象,十分麻烦,其实还可以利用匿名对象去调用,如下:

方法二:通过访问限定符;

场景二:想要为函数的形参给缺省参数,而形参为自定义类型,其缺省参数又该如何呢?

一般给缺省参数是要给常量、字面量等固定的值,而倘若对于自定义类型的缺省值可以考虑使用匿名对象;

匿名对象作为缺省参数;

上图中用的都是传值传参,对于自定义类型数据来用引用传参比较好(如果该自定义类型非常大,那么拷贝构造就会有较大的消耗),同理,需要使用const 引用

Q:函数func2 的形参部分是否可以使用显式类型转换?

  • 此处可以,如下图;但是学了模板之后就不可以了,倘若模板并不确定此类型,便就不可以使用类型转换;

此处给缺省值不能利用有名对象,因为有名对象为变量,而匿名对象为常量;

八、对象拷贝时的编译器的优化

1、知识点汇总:

  • 1、现代编译器为了尽可能地提高效率,在不影响正确性的情况下会尽可能减少一些传参和传返回值的过程中可以省略的拷贝
  • 2、如何优化c++标准并没有定义,各个编译器都会根据自身情况自行处理;当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续构造与拷贝会进行合并优化,有些根性更“激进”的编译器还会进行跨越表达式的合并优化;
  • 3、Linux 下可以将下面的代码拷贝到 test.cpp 中,编译时用 g++ test.cpp -fno-elide-constructors 的方式关闭构造相关的优化;

2、详细理解:

场景一:构造+拷贝构造

代码如下:

class A
{
public:
	//构造
	A(int a = 0)
		:_a1(1)
	{
		cout << "A(int a = 0)" << endl;
	}
	//拷贝构造
	A(const A& aa)
		:_a1(aa._a1)
	{
		cout << "A(const A& aa)" << endl;
	}
	//赋值运算符重载
	A& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a1 = aa._a1;
		}
		return *this;
	}
	//析构
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a1 = 1;
};

int main()
{
	//隐式类型转换,原应该为构造临时对象然后拷贝构造aa1
	//看一下优化之后会怎么走?
	A  aa1 = 1;
	return 0;
}

运行结果如下:

VS下的编译器将构造+拷贝构造优化成了直接调用构造;

在类型转换的时候会发生编译器的优化;理论上类型转换会产生临时对象,然后再利用该临时对象拷贝构造,但实际上是直接的构造;

在VS编译器下不好观察,我们还可以借助于Linux 来观察:

运行代码还是和上面的一样,只不过运行环境改为了Linux, 运行结果如下:

在Linux 之中也是将构造+拷贝构造优化成了直接构造;

我们还可以在编译的时候用 g++ test.cpp -fno-elide-constructors 的方式关闭构造的相关优化,运行结果如下: 

临时对象的生命周期和匿名对象的生命周期一样,只有一行,当走到下一行的时候,临时对象便会被销毁即自动调用析构函数;

注:系统自动产生的临时对象,我们也可以自己创建同样效果的叫做匿名对象;它们均具有常性,功能作用比较相似;

Q:除了构造+拷贝构造会被优化为直接的构造,还有哪些场景下会有这样的优化呢?

场景二:在传参的过程之中也会有优化

代码如下:

#include <iostream>
using namespace std;

class A
{
public:
    //构造
    A(int a = 0)
    :_a1(a)
    {
        cout<<"A(int a = 0)"<<endl;
    }
    //拷贝构造
    A(const A& aa)
    :_a1(aa._a1)
    {
        cout<<"A(const A& aa)"<<endl;
    }
    //赋值运算符重载
    A& operator=(const A& aa)
    {
        cout<<"A& operator=(const A& aa)"<<endl;
        if(this != &aa)
        {
            _a1 = aa._a1;
        }
        return *this;
    }
    //析构
    ~A()
    {
        cout<<"~A()"<<endl;
    }

private:
    int _a1 = 1;
};

void f1(A aa)
{}

int main()
{
    //有名对象来调用函数
    A aa1(1);
    f1(aa1);

    //隐式类型转换
    f1(1);
    //匿名对象
    f1(A(1));
    return 0;
}

有名对象调用函数在VS2022中的运行结果:

因为是使用有名对象来调用函数f1 ,故而整体的逻辑为:构造对象aa1,然后因传值传参就会调用拷贝构造;

隐式类型转换在VS2022中的调用结果:

隐式类型转换的传值传参,逻辑上首先会构造出一个临时对象,然后再将这个临时对象拷贝构造函数func 中的形参,但是被编译器优化成了直接构造

匿名对象在VS2022中的调用结果:

匿名对象传值传参,逻辑上为:先是调用构造函数来创建匿对象,然后传值传参调用拷贝构造;实际上被编译器优化成了直接的构造;

所以连续的构造+拷贝构造会被编译器优化为直接的构造;

在Linux 下不做优化的运行结果如下:

场景三:传值返回

代码如下:

class A
{
public:
    //构造
    A(int a = 0)
        :_a1(a)
    {
        cout << "A(int a = 0)" << endl;
    }
    //拷贝构造
    A(const A& aa)
        :_a1(aa._a1)
    {
        cout << "A(const A& aa)" << endl;
    }
    //赋值运算符重载
    A& operator=(const A& aa)
    {
        cout << "A& operator=(const A& aa)" << endl;
        if (this != &aa)
        {
            _a1 = aa._a1;
        }
        return *this;
    }
    //析构
    ~A()
    {
        cout << "~A()" << endl;
    }

private:
    int _a1 = 1;
};

//传值传参
void f1(A aa)
{}

//传值返回
A f2()
{
    A aa;
    return aa;
}

int main()
{
    f2();
    return 0;
}

需要注意的是,我们此处只是单纯的调用函数f2 ,并没有使用变量接收其返回值

在VS2022中的运行结果如下:

显然,在上述调用f2 的过程中,逻辑上首先会调用构造来创建A类对象,然后传值返回的时候首先会调用拷贝构造函数生成临时对象,然后将这个临时对象作为返回值;此处编译器优化地很狠,将创建临时对象的构造、创建临时兑现的拷贝构造直接优化成了一个构造;

但是在VS2019Debug 版本之下,就不会进行优化;因为优化并不只语言规定的,而是编译器自己自行去做的,即优化取决于编译器,不同的编译器会有不同的动作;对于VS2019 来说,局部临时对象的构造+拷贝构造临时对象并未被优化,但是对于VS2022来说,会被直接优化;

在Linux 不优化的运行结果:

在Linux 下不做优化就是构造+拷贝构造;

需要注意的是,对于新一点的编译器,若你调用一个传值返回的函数而并未使用变量接收,那么编译器就有可能不会产生临时对象将返回值带回;

Q:倘若使用了变量接收这个函数的返回值会怎么样?

理论上,此处应该是 构造+拷贝构造+拷贝构造

在VS2019 Debug 之下运行结果如下:

在Linux 下不做优化的运行结果如下:

Q:为什么在VS2019 中运行结果是构造+拷贝构造?为什么少调用了一个构造?

  • 仔细思考不难发现,上述的拷贝构造生成的临时对象有一点多余;所以编译器就没有调用拷贝构造去创建临时对象,而是直接进行了下一步接收函数f2 的返回值对象的拷贝构造;

Q:在VS2022上运行呢?

结果如下:

我们可以经过以上的测试而总结出编译器的优化:

上图说明了,编译器在不断地优化,其优化需要保证不影响代码结果的正确性

我们还可以结合函数栈帧来理解,如下:

其中对于优化2,总之就是一个“合三为一”非常极限的优化,省去了构造aa ,拷贝构造临时对象,拷贝构造aa2 的过程,而直接构造aa2 ;

Q:不构造局部变量aa ,倘若在函数f2 中要使用aa ,那该怎么办呢?

  • 其底层实现的角度来看:并没有构造对象aa ,而是直接构造对象aa2 , aa 是aa2 的引用;

我们可以分别打印变量 aa 、aa2 的地址来看,如下:

显然,对象aa 与对象aa2 的地址是相同的,那么aa 就是对象aa2 的引用;

        最初祖师爷在设计此语法逻辑、编译器实现,在早期的时候并没有考虑要进行优化,只是做到了将语法解释够了便就可以了;

        变量aa 是一个局部对象,出了作用域便会被销毁,而函数f2 是需要有一个返回值,不能用局部变量aa 作为返回值。故而在语层面上就会返回一个aa 的拷贝(在函数f2 结束之前利用对象aa拷贝构造一个临时对象,倘若这个临时对象比较小便会放在寄存器之中,如果比较大便会放在main 函数的函数栈帧之中【要保证该临时对象不受函数f2 函数栈帧销毁的影响】,可以是理解为当这个临时对象比较大的时候会放在main 函数与f2 函数的函数栈帧之中一个函数有返回值的话就会在当前这个函数的函数栈帧销毁之前将产生的临时对象压入上一层函数栈帧之中,而该临时对象会作为f2 函数调用的返回值拷贝构造aa2(当临时对象完成拷贝之后便会被销毁,因为临时对象的生命周期只有一行);

临时对象的特点:

  • 1、临时对象具有常性
  • 2、临时对象的生命周期只有其所在的一行

Q:实际当中,我们所写的有哪些动作可能会干扰编译器的优化?
例子代码如下:

class A
{
public:
    //构造
    A(int a = 0)
        :_a1(a)
    {
        cout << "A(int a = 0)" << endl;
    }
    //拷贝构造
    A(const A& aa)
        :_a1(aa._a1)
    {
        cout << "A(const A& aa)" << endl;
    }
    //赋值运算符重载
    A& operator=(const A& aa)
    {
        cout << "A& operator=(const A& aa)" << endl;
        if (this != &aa)
        {
            _a1 = aa._a1;
        }
        return *this;
    }
    //析构
    ~A()
    {
        cout << "~A()" << endl;
    }

private:
    int _a1 = 1;
};

//传值传参
void f1(A aa)
{}

//传值返回
A f2()
{
    A aa;
    return aa;
}

int main()
{
    //此处就是先构造然后再赋值
    A aa2;
    aa2 = f2();
    return 0;
}

运行结果如下:

在VS2019 debug 下:

在VS2019 release、VS2022 debug、release下:

此时的优化就仅仅只是直接赋值,干掉了临时对象的创建,不敢优化为直接构造,因为这样做可能会出问题;例如在调用f2 函数之前,会实际用到对象aa2;

所以不建议写成:而是建议这么写:了解编译器的优化机制,我们在写代码的时候就可以朝着更优的方向去写,对于此处而言,倘若想要再用一个变量去接收传值返回函数的返回值,最好是定义一个对象去直接接收,而不是定义了一个对象之后在下一行利用赋值来接收;


总结

1、const 成员函数:用const 修饰的成员函数;const 修饰成员函数放到成员函数参数列表的后面;const 实际修饰该成员函数隐含的this 指针,表明该成员函数中不能对类的成员任何成员变量进行修改

2、取地址运算符重载分为普通地址运算符重载const 取地址运算符重载一般这两个函数编译器自动生成的就够用了,无需我们显式实现除非你不想让别人取到当前类对象的地址,这个时候你就可以随便返回一个地址

3、引用成员变量,const 成员变量,没有默认构造的类类型变量必须放在初始化列表位置进行初始化,否则就会编译报错;

4、初始化列表中按照成员变量在类中声明的顺序进行初始化,跟成员变量在初始化列表中出现的先后顺序无关

5、构造函数前面加explicit 就不再支持隐式类型转换,但是仍然支持显式类型转换;类类型的对象之间也可以隐式类型转换,需要相应的构造函数支持;

6、用static 修饰的成员变量,称之为静态成员变量,静态成员变量一定要在类外进行初始化;静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象之中,存放在静态区;用static 修饰的成员函数,称之为静态成员函数静态成员函数没有this 指针

7、如果一个类定义在另外一个类的内部,这个在另外一个类里面的类就叫做内部类。内部类是一个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符的限制,所以外部类定义的对象中不包含内部类;2、内部类默认是外部类的友元类, 即内部类可以访问外部类的所有成员

8、类型(实参)定义出来的对象叫作匿名对象,相比之前我们定义的类型+对象名(实参)定义出来的就是有名对象匿名对象的生命周期只在当前一行

9、现代编译器为了尽可能地提高效率,在不影响正确性的情况下会尽可能减少一些传参和传返回值的过程中可以省略的拷贝;如何优化c++标准并没有定义,各个编译器都会根据自身情况自行处理;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值