目录
- 1.初始化列表
- 1.1构造函数赋值
- 1.2初始化列表
- 格式:
- 编译器执行的顺序:
- 特性:
- 1.3explicit关键字
- 类型替换过程
- 多参数构造函数类型替换(C++11)
- 2.static成员
- 编程题
- 3.匿名对象
- 4.友元
- 4.1友元函数
- 4.2友元类
- 5.内部类
- 6.拷贝对象时的一些编译器优化
- 6.1传值传参
- 6.2传值返回
- 7.理解类和对象
1.初始化列表
1.1构造函数赋值
创建对象时,编译器会调用构造函数来为对象中的成员变量一个合适的初始值。
但不是所有的成员变量都可以在构造函数内获得初始值的,如下代码:
运行后给出如下错误
这是因为被const修饰的变量只能在定义的时候初始化,那哪里是成员变量定义的地方?
答:成员变量在初始化列表 中定义并初始化。
如下图,是我们经常写的类:
在类内private
访问限定符下的成员变量只是成员变量的声明 ,在对象创建时(也叫定义对象)会调用类内的构造函数(默认构造函数或非默认构造函数),其中在 成员变量的定义 的位置,也叫做初始化列表 (下面会讲,现在知道这个位置即可),成员变量在这个地方定义的,如果成员变量中有被const修饰的可以在此处初始化。
了解了这些,我们在来看一下初始化列表的具体特性和功能。
1.2初始化列表
格式:
初始化列表:在构造函数(默认构造函数或非默认构造函数)下以一个冒号开始 ,接着是一个以 逗号分隔的数据成员列表 ,每个“成员变量 ”后面跟一个放在括号中的初始值或表达式。
class Date
{
public:
Date()
:_year(1)
,_month(1)
,_day(1)
{}
private:
int _year;
int _month;
int _day;
};
编译器执行的顺序:
-
之所以在这个位置以该格式写的代码被称为初始化列表,一是因为在这里成员变量完成定义,其次是编译器在对象创建后调用构造函数也是先执行初始化列表 ,将对应的成员变量初始化后,在执行构造函数体内的代码。
如下图:
-
如果我们没有实现初始化链表,编译器会默认实现,因为对于成员变量必须要定义,而初始化列表就是定义成员变量的,只是这样初始化后的成员变量值为随机值,需要看它的构造函数或其他函数是否为其赋值。
但要注意,我们没有实现初始化列表,遇到被const修饰的成员函数或其他特殊的情况(下面会讲),可就运行不了了。
结论: 尽量使用初始化列表。
-
成员变量 在类中声明次序 就是其初始化列表中的初始化顺序 ,与其在初始化列表中的先后次序无关。
如下图,调换初始化列表的顺序,可以观察到,程序的执行顺序为
_year
、_month
、_day
与声明顺序相同。
特性:
-
每个成员变量在初始化列表中只能出现一次
初始化只能初始化一次,第二次就成复制了,赋值是在构造函数体内完成。
-
类中如果包含以下成员,必须放在初始化列表位置处初始化。
-
引用成员变量
引用的特点就是在定义时必须赋值。
-
const成员变量
被const修饰的变量,如果不在定义时初始化,其他时候无法改变它的值,那该变量的存在就没有价值。
-
自定义类型成员(且该类中没有默认构造函数时)
没有默认构造函数,意味着有非默认构造函数,编译器无法在其定义时自动初始化该成员变量,只能通过我们自己传值调用其非默认构造函数。
class A { public: A(int a) : _a(a) {} private: int _a; }; class B { public: B(int a, int ref) : _Aobj(a) , _ref(ref) , _n(1) {} private: A _Aobj; //没有默认构造函数 int& _ref; //引用 const int _n; //const };
-
-
注意: 尽量使用初始化列表,如果不能用初始化列表初始化的成员变量,如需要开辟空间的情况等等,在构造函数体内完成即可,不要只盯着初始化列表。
拓展:
C++11中规定内置类型成员变量在类中声明时可以给默认值。
这里的默认值是缺省值,不是初始化,比方说对于被const修饰的成员变量,如果在声明时给出默认值,并在初始化列表初始化,最后以初始化列表为主,若是初始化列表没有初始化则以默认值为主
class Date
{
public:
Date()
: _year(1)
{
cout << _year << endl;
}
private:
const int _year;
int _month;
int _day;
};
class A
{
public:
A()
{
cout << _year << endl;
}
private:
const int _year = 2;
};
int main()
{
Date today;
A a;
return 0;
}
1.3explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数(统称:单参数默认构造函数),还具有类型替换的作用。
若不想实现这种功能,在构造函数前增加关键字
explicit
即可
类型替换过程
接下来我们一步步探索这个现象
我们先来看如下代码
int main()
{
int a = 1;
double b = a; //隐式类型转化
return 0;
}
这个代码是可以运行成功的,变量a在为b赋值时,发生隐式类型转化,首先生成了一个const double类型的临时变量(临时变量有常性 ),变量a将值赋给这个临时变量,由临时变量将值赋给变量b
了解了临时变量的存在,在来看下面的一段代码
class A
{
public:
A(int a)
: _a(a)
{
cout << "A(int a)" << endl;
}
private:
int _a;
};
int main()
{
const A& b = 1;
return 0;
}
我们看到,代码成功运行并输出,而想要为引用初始化,需要为其赋一个同类型的变量,所以const A& b = 1;
中,先创建一个临时的A类型对象(临时的对象也有常性,所以引用需要使用const修饰),并使用1为其成员变量赋值,这个过程调用构造函数,接着使对象b成为零时对象的引用,证明这么做必然会产生一个临时的变量。
到这里类型替换的过程也就呼之欲出了,不过结果可能和大家想象的不一样。
看如下代码
class A
{
public:
A(int a)
: _a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
: _a(aa._a)
{
cout << "A(const A& a)" << endl;
}
private:
int _a;
};
int main()
{
A a = 1;
//等价于: A a(1);
return 0;
}
按照正常的想法,通过构造函数创建一个临时对象,在通过拷贝构造函数创建对象a,而现在只调用了构造函数,这是为什么?
这是编译器帮我们做出的优化,正常的过程是构造->拷贝 = a
,但C++觉得这么做有些繁琐,便取消了拷贝的过程,直接通过1来构造对象a ,等价于A a(1)
。
- 在C++发展之初这些优化是没有的,但随着发展,编译器不断进化,开始减少很多没必要的步骤,慢慢做出优化,同时对语法的要求更高,保证优化后对编译没有影响。
- 一般新一点的编译器会有优化,老的编译器没有。
多参数构造函数类型替换(C++11)
对于多参数的构造函数,C++98是不支持进行类型替换的,但在C++11对这一块进行拓展,支持其多参数进行这一操作。需在进行多参数替换时,使用大括号包含需要传的参数即可。如下
- 注意:若不想进行替换操作,在构造函数前增加explicit关键字即可
2.static成员
声明为static的类成员 称为 类的静态成员 ,用static 修饰的 成员变量 ,称之为 静态成员变量 ;用 static修饰 的 成员函数 ,称之为 静态成员函数 。静态成员变量一定要在类外进行初始化。
其特性如下:
- 静态成员 为 所有类对象共享 ,不属于某个具体的对象,存放在静态区
- 静态成员变量 必须在类外定义,定义时不添加static关键字,类中只是声明
- 类静态成员即用 类名::静态成员 或者 对象.静态成员 来访问
- 静态成员函数 没有 隐藏的 this指针,不能访问非静态成员
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制
了解了特性我们需要注意以下几种情况:
-
创建类的指针对象,设置为空,它可以访问类中的静态成员和静态函数
class A { public: static void Print() { cout << _a << endl; } private: static int _a; }; int A::_a = 0; int main() { A* a = nullptr; a->Print(); return 0; }
静态成员为所有类对象共享,使用空指针调用,不涉及指针自生,甚至在静态成员函数内不会有this指针,不会造成编译错误。
-
静态成员函数不能调用非静态成员函数
静态成员函数内没有this指针,无法调用除静态成员外的其他成员
-
非静态成员函数可以调用类的静态成员函数
静态成员是所有类对象共享,所有对象都可以调用,非静态成员内有this指针,为类对象指针,可以调用静态成员,甚至非成员函数,普通函数或其他类的成员函数都可以通过
类名::静态成员
的方法调用一个类的静态成员
了解了这些,我们使用静态成员来做一道编程题:
编程题
求1+2+3+…+n_
描述:
求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句
(A?B:C)。
数据范围: 0<<n≤200
进阶: 空间复杂度 O(1) ,时间复杂度 O(n)
示例1
输入: 5
返回值:15
示例2
输入:1
返回值:1
思路:
我们创建一个类,设置两个静态成员变量,一个sum存放所有数相加的和,一个i表示当前需要加几,编写其构造函数,使创建一次对象sum加一次i,同时使i自加1,之后创建n个这个类的对象,最后得到的sum就是总和,因为成员变量一般放在private限定符下,所以在通过创建一个静态成员函数来返回这个静态成员变量sum。
注意: 创建n个对象可以使用变长数组 类名 arr[n]
或 new
,变长数组是c99中定义的,是否可以使用取决于编译器是否支持,在牛客网中的编译器支持这一语法。
代码:
class Sum
{
public:
Sum()
{
_sum += _i;
_i++;
}
static int Print()
{
return _sum;
}
private:
static int _sum;
static int _i;
};
int Sum::_sum = 0;
int Sum::_i = 1;
class Solution {
public:
int Sum_Solution(int n) {
Sum arr[n];
return Sum::Print();
}
};
3.匿名对象
我们创建对象还可以通过类名() 的方式创建,这样的对象叫匿名对象,声明周期 只有被创建的哪一行,该行执行完,对应的匿名对象就会被销毁。
class A
{
public:
A()
{
cout << "A()" << endl;
}
A(int a, int b)
: _a(a)
, _b(b)
{
cout << "A(int a, int b)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
int _b;
};
int main()
{
A(); //匿名对象
A(1, 2); //匿名对象
return 0;
}
使用场景:
如果我们需要创建对象,但对象创建后只使用一次,那就可以用匿名对象
注意:
匿名对象具有常性 ,需要使用引用接收匿名对象时需要使用const修饰。
4.友元
友元提供了一种突破封装的方法,有时提供便利,但友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数 和 友元类
4.1友元函数
友元函数 是定义在类外部的普通函数 ,不属于任何类,但需要在类的内部声明,声明时需要加friend 关键字。友元函数 可以 直接访问 类的私有 成员。
特性:
- 友元函数 内类对象可访问类的私有和保护成员,但它不是类的成员函数
- 友元函数不能用const修饰
- 友元函数 可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
class A
{
public:
friend void test(A& a); //友元函数的声明
A(int a,int b)
:_a(a)
,_b(b)
{
}
private:
int _a;
int _b;
};
void test(A& a)
{
cout << a._a << endl;
}
int main()
{
A a(1, 2);
test(a);
return 0;
}
4.2友元类
友元类所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
注意:
-
友元关系是单向的,不具有交换性。
-
友元关系不能传递
如果A是B的友元,B是C的友元,则不能说明A是B的友元
如果A是B的友元,C也是B的友元,也不能说明A和B一个是另一个的友元。
-
友元关系不能继承
class A
{
public:
friend class B;//友元类的声明
A()
: _Aa(1)
, _Ab(2)
{}
private:
int _Aa;
int _Ab;
};
class B
{
public:
B()
: _Ba(1)
, _Bb(2)
{
A aa;
cout << aa._Aa << " " << aa._Ab << " " << endl;
}
private:
int _Ba;
int _Bb;
};
int main()
{
B bb;
return 0;
}
5.内部类
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
class A
{
public:
A()
: _Aa(1)
, _Ab(2)
{}
class B
{
public:
B()
: _Ba(1)
, _Bb(2)
{
A aa;
cout << aa._Aa << " " << aa._Ab << " " << endl;
}
private:
int _Ba;
int _Bb;
};
private:
int _Aa;
int _Ab;
static int _Ac;
};
特性:
-
内部类就是外部类的友元,内部类可以通过外部类的对象参数来访问外部类中的所有成员,但外部类不是内部类的友元。
-
内部类可以定义在外部类的public、protected、private都是可以的
-
想要创建一个内部类对象,需要通过外部类指定
int main() { A::B bb;//创建内部类 return 0; }
-
注意内部类可以直接访问外部类中的static成员,不需要外部里的对象/类名。
注意:友元类不可以直接访问static成员
-
sizeof(外部类) = 外部类,和内部类没有任何关系。
int main() { cout << sizeof(A) << endl; return 0; }
- 对于内部类,我们只做了解就好,在C++中很少用到内部类,而java中内部类用的倒是很多。
6.拷贝对象时的一些编译器优化
在传参和传返回值的过程中,一般编译器会做出一些优化,减少对象的拷贝,这个在一些场景下可以帮我们提高程序的运行速率。
创建如下类,通过下面这个类创建的对象进行操作,观察现象,搞清楚编译器的优化
class A
{
public:
A(int aa = 0)
:_a(aa)
{
cout << "A()" << endl;
}
~A()
{
cout << "~A" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& a)" << endl;
}
A& operator=(const A& aa)
{
if (&aa != this)
{
_a = aa._a;
}
cout << "A& operator=(const A& a)" << endl;
return *this;
}
private:
int _a;
};
在上面讲explicit
关键字时,我们已经知道了类型替换的概念,知道编译器会优化一些步骤使之更加便捷,下面我们针对这一现象再来研究一下
6.1传值传参
void test1(A aa)
{}
void test3(const A& aa)
{}
int main()
{
A aa1;//构造
test1(aa1);//拷贝构造
cout << "-----------------" << endl;
test3(aa1);//引用传参,对象已存在无需进行构造或拷贝
cout << "-----------------" << endl;
test1(A(1));//匿名对象构造+拷贝构造 -> 优化为直接构造
cout << "-----------------" << endl;
test1(1);//隐式类型,构造+拷贝构造 -> 优化为直接构造
return 0;
}
对于正常的创建对象,之后将对象传递给参数,这样的操作,虽然也进行了构造和拷贝两个步骤,但它是在两行上分别执行的,绝大多数编译器都不会在此处进行优化。(一些比较激进的编译器会这样做)
对于引用传参,因为对象已经创建好了,直接引用即可,只执行了一个构造,而且函数执行完后还可以接着使用,但注意,若传过去的对象不会改变,建议使用const修饰,防止发生变化。
对于使用匿名对象传参 或隐式类型替换 为对象的操作,正常来看都需要经过构造、拷贝两个步骤,形参才会被赋予数据,而经过编译器优化后省去了拷贝的步骤,直接使用初始化的值构造形参。但对象在函数执行完后就无法在使用。
6.2传值返回
A test2()
{
A a;
return a;
}
A test4()
{
return A(1);
}
int main()
{
test2();//构造+拷贝构造
cout << "-----------------" << endl;
A aa2 = test2();//构造+拷贝构造+拷贝构造 -> 优化为:构造 + 拷贝构造
cout << "-----------------" << endl;
A aa1;//构造
aa1 = test2();//构造+拷贝构造+赋值重载
cout << "-----------------" << endl;
A aa3 = test4();//使用匿名对象返回,构造+拷贝+拷贝 -> 构造
cout << "-----------------" << endl;
return 0;
}
对于传值返回中的第16行代码,返回值由一个未定义的对象接收,意味着以返回对象为参数,调用拷贝构造函数,定义该对象,所以该行代码的执行步骤应该为:构造、拷贝、拷贝编译器对其进行优化,将原本test2中返回时需要拷贝出一个临时对象的操作,优化为直接拷贝出所接收的对象,将两个拷贝优化为一个拷贝。
对于第19行,因为对象以及定义,使用定义好的对象接收返回对象,这里就是赋值重载,对于赋值重载无法进行优化。
对于21行,接收匿名函数返回的对象,原本应该是匿名对象调用构造函数创建对象,之后拷贝一个临时对象,由临时对象在经过拷贝创建出所要的对象,经过编译器优化,两次拷贝都不在调用,因为匿名对象只执行一行,对象返回后就会被销毁,编译器直接使用匿名对象构造出的对象作为接收返回值的对象,所得结果相同,但过程更为简洁。
总结:
-
对象传参
尽量使用引用接收参数
-
对象返回
- 接收返回值对象,尽量使用拷贝方法接收,不要使用赋值接收
- 函数中返回对象时,尽量返回匿名对象。
7.理解类和对象
对于现实生活中的实体,计算机是不认识的,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。
比如说我们使用的手机实体,想要认识让计算机认识手机,需要经过以下步骤:
- 先对现实生活中手机实体进行抽象—即在人为思想层面对手机进行认识,手机有什么属性,有那些功能,即对手机进行抽象认知的一个过程。
- 经过1后,在人的头脑中已经对手机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的手机,就需要人通过某种面向对象的语言(如:Java、C++、Python等)将手机用类来进行描述,并输入到计算机中。
- 经过2后,在计算机中就有了一个手机类,但手机类只是站在计算机的角度对手机对象进行描述的,通过手机类,可以实例化出一个个具体的手机对象,此时计算机才能知道手机是什么东西。
- 此时用户就可以借助计算机中手机对象,来模拟现实中的手机实体了。
在类和对象阶段,大家一定要体会到:
类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,用该自定义类型就可以实例化具体的对象。