- 初始化列表
- 静态成员(变量和方法)
- 友元函数和友元类
- 内部类
- 匿名对象
- 拷贝对象时一些编译器的优化
一.初始化列表
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 3, int b = 3)
{
_a = a;
_b = b;
}
private:
int _a;
int _b;
};
int main()
{
A aa;
return 0;
}
上面17行中的对象实例化:编译器先会整体开辟空间,这时还没有定义成员变量,在调用构造函数时,才会单独定义每个成员变量,那么在哪定义成员变量呢?上面7,8行是定义成员变量吗?
- c++规定成员变量的定义和初始化是在初始化列表来进行的(不显式写初始化列表,编译器也会在那里定义成员变量)。
- 上面7,8行代码,实际上是赋值。在初始化列表将他们定义后,7.8行处的代码是变量之间的赋值,初始化只能有一次,而变量的赋值可以有多次。
- 按照上面在构造函数体内给成员变量赋值,有时候是不行的。比如:当成员变量为引用类型或者const类型或者没有默认构造函数的自定义类型时,因为引用和const类型的变量必须初始化,而没有默认构造函数的自定义类型无法进行默认构造就会报错。
1.1初始化列表的定义
初始化列表:以一个:
开始,接着是一个以,
分割的数据成员列表,每个成员变量后跟一个放在括号内部的初始值或表达式。
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 3, int b = 3)
:_a(a)
, _b(b)
, _c(_b)
{
}
private: //声明成员变量
int _a;
int _b;
int& _c;
};
int main()
{
A aa;
return 0;
}
1.2总结
- 初始化列表中每个成员变量只能出现一次(初始化只能有一次)
- 初始化顺序是按照声明成员变量的顺序,与初始化列表中的顺序无关
- 引用,const,自定义类型(无默认构造函数) 有这三个成员变量必须使用初始化列表
- 最好使用初始化列表进行初始化,对于自定义类型一定要使用初始化列表进行初始化。
二.静态成员
声明为static的类成员叫做静态成员,用static修饰的成员变量叫做静态成员变量,用static修饰的成员函数叫做静态成员函数,静态成员也是成员所以也受public,protected,private的限制
2.1静态成员变量
静态成员变量不能在初始化列表初始化,只能在类外初始化,不加static
- 静态成员变量不属于某个对象,而是属于整个类,被所有类对象共享,存放在静态区
- 静态成员变量在类外访问:
1. 可以通过对象访问,但是必须是公有静态成员才可以
2. 可以通过类名加::访问
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 3, int b = 3)
:_a(a)
, _b(b)
, _c(_b)
{
}
private: //声明成员变量
int _a;
int _b;
int& _c;
public:
static int _d; //静态成员变量的声明
};
//静态成员变量的初始化
int A::_d = 3;
int main()
{
A aa;
//以下两种都可以访问静态成员变量
cout << A::_d << endl;
cout << aa._d << endl;
return 0;
}
2.2静态成员函数
- 静态成员函数没有隐藏的this指针,所以它不能访问类中非静态成员变量,也不可以调用其他非静态成员函数
- 受public,private,protected的影响
- 可以通过类名加::访问,也可以通过对象访问。
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 3, int b = 3)
:_a(a)
, _b(b)
, _c(_b)
{
}
static void Print()
{
_d = 5;
cout << _d << endl;
}
private: //声明成员变量
int _a;
int _b;
int& _c;
static int _d; //静态成员变量的声明
};
//静态成员变量的初始化
int A::_d = 3;
int main()
{
A aa;
//以下两种方法均可访问静态成员函数
aa.Print();
A::Print();
return 0;
}
三.友元函数和友元类
友元机制可以在类外访问类内私有的成员,有时可以提供遍历,但是这破坏了面向对象封装的思想,增加了耦合度,所以友元机制不应该多用。
3.1流插入(<<)和流提取(>>)的重载函数
cout 是一个ostream类型的流对象,cin是一个istream类型的流对象,可知cin和cout也是自定义类型的对象,之所以可以用<<和>>运算符,是因为在库中提供了>>和<<对于内置类型的重载函数,我们如果想要
输出或者输入自定义类型,我们需要自己写一个重载函数。
类内实现重载函数
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{
}
//在类内实现
ostream& operator<<(ostream& out)
{
out << _year << _month << _day << endl;
return out;
}
istream& operator>>(istream& in)
{
in >> _year >> _month >> _day;
return in;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
//类内实现重载函数,这种调用方法是错误的
//cout << d; 因为this指针是左操作数
d << cout; 这种才是正确的调用方法,但是很奇怪
d >> cin;
}
上面在类内实现流插入和流提取的重载函数,由于this指针是左操作数,所以调用方式是反的,如果按照正常的调用顺序,可以在cout/cin所在类中重载Date类型对象,但是我们无法写入。所以只能将这两个重载函数实现在类外。访问类内的私有成员:1.增加接口 2.友元机制
#include<iostream>
using namespace std;
class Date
{
//友元声明
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{
}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << d._month << d._day << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
int main()
{
Date d;
cout << d;
cint >> d;
}
3.2友元函数
友元函数可以直接访问类的私有成员和保护成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
- 友元函数可以访问类内private和protected成员,但不是类的成员
- 友元函数不能用const修饰。没有this指针
- 友元函数不受访问限定符的限制,可以在任何地方访问。
- 一个函数可以是多个类的友元函数
- 友元函数和普通的函数调用相同,只是声明时加friend关键字。
3.3友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
- 友元关系是单向的
- 友元关系不能继承
- 友元关系不能传递
#include<iostream>
using namespace std;
class A
{
//友元类的声明
friend class Date;
};
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;
};
int main()
{
Date d;
}
四.内部类
4.1定义
内部类:一个类定义在另一个类的内部,这个类就叫做内部类。
- 内部类是一个独立的类,不属于外部类。在计算外部类的大小时,可以忽略内部类
- 内部类天生就是外部类的友元类。
- 内部类的作用域是外部类,也就是说在外部类的类外访问内部类时,需要指定外部类::
- 内部类访问外部类的静态成员,不需要加对象或者类名
- 内部类受访问限定符的限制
4.2代码
#include<iostream>
using namespace std;
class A
{
private:
static int k;
int h;
//private:
public:
class B // B天生就是A的友元
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a.h << endl;//OK
}
};
};
int A::k = 1;
int main()
{
A::B b; //因为内部类是public权限,所以可以访问,如果是private则不能访问
b.foo(A());
return 0;
}
五.匿名对象
类名()
就可以定义一个匿名对象,生命周期只有一行
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
A aa; //实例化一个aa对象,
A(); //实例化一个匿名对象,生命周期只在这一行
}
六.拷贝对象时的一些编译器优化
6.1explicit关键字
构造函数不仅可以构造初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
class A
{
public:
// 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用
// explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译
A(int a)
:_a(a)
{}
/*
// 2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具
有类型转换作用
// explicit修饰构造函数,禁止类型转换
explicit Date(int a, int b = 1, int c = 1)
: _a(a)
, _b(b)
, _c(c)
{}
*/
//c++11,新增了多参数隐式类型转换
A(int a, int b, int c)
:_a(a)
{}
A& operator=(const A& d)
{
if (this != &d)
{
_a = d._a;
_b = d._b;
_c = d._c;
}
return *this;
}
private:
int _a;
int _b;
int _c;
};
void Test()
{
A d1(2);
// 用一个整形变量给A类型对象赋值
A d2 = 1; //这里发生了隐式类型转换
A d3 = {1, 2, 3}; //这是多参数构造函数
// 实际编译器背后会用1构造一个匿名对象,最后用匿名对象给d2对象进行拷贝构造
}
- 这里
A d2 = 1
,发生了构造函数的隐式类型转换,如果我们不想让这个隐式类型转换发生,我们可以在构造函数前面加explicit
关键字,- c++98,提供了单参数构造函数的隐式类型转换,c++11又新增了多参数隐式类型转换
6.2函数传参时,编译器的优化
对于在一行代码中,连续出现:构造+构造;拷贝构造+拷贝构造;构造+拷贝构造等等,编译器会将它们进行优化。
#include<iostream>
using namespace std;
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;
};
void f1(A aa)
{}
int main()
{
// 传值传参
A aa1;
f1(aa1);
cout << endl;
// 隐式类型,连续构造+拷贝构造->优化为直接构造
f1(1);
// 一个表达式中,连续构造+拷贝构造->优化为一个构造
f1(A(2));
cout << endl;
return 0;
}
- 37-49行:38行进行了构造,39行传参时进行了拷贝构造,但是由于不是一个表达式,所以编译无法优化
- 43行:首先1隐式类型转换调用构造函数后生成一个临时对象,临时对象拷贝构造参数aa,这里编译器直接优化为一个构造
- 45行:A(2)调用构造生成匿名对象,然后匿名对象拷贝构造aa,优化为一个构造
6.3传返回值时,编译器的优化
#include<iostream>
using namespace std;
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;
};
A f2()
{
//法一:
A aa;
return aa;
//法二:
//返回匿名对象
// return A();
}
//函数返回不是直接返回变量,而是形成一个临时变量返回
int main()
{
A aa1;
// 传值返回
f2();
cout << endl;
// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
A aa2 = f2();
cout << endl;
// 一个表达式中,连续拷贝构造+赋值重载->无法优化
aa1 = f2();
cout << endl;
return 0;
}
- 47行:
- 方法一:构造+拷贝构造(临时变量),不在一个表达式,无法优化。
- 方法二:构造+拷贝构造,在一个表达式,直接优化为一个构造
- 52行:
- 方法一:构造+拷贝构造+拷贝构造,后两个连续的拷贝构造在一个表达式,所以优化为构造+拷贝构造
- 方法二:构造+拷贝构造+拷贝构造,这三个在一个表达式中,直接优化为一个构造
- 55行:
- 方法一:构造+拷贝构造+赋值重载,虽然拷贝构造和赋值重载在一个表达式,但是无法优化
- 方法二:构造+拷贝构造+赋值重载,编译器会将构造+拷贝构造优化为一个构造,所以最后的结果是构造+赋值重载
6.4总结
传值返回时:
- 函数中返回对象时,尽量返回匿名对象
- 接收返回值对象时,尽量用拷贝构造接收,不要赋值重载
函数传参时:
- 尽量用引用传参