我们最后来到C++类和对象知识的收官之篇
目录
一、再来谈谈构造函数
1.1 使用构造函数体赋值
1.2 初始化列表
1.3 explicit关键字
二、static成员
三、匿名对象
四、友元
4.1 友元函数
4.2 友元类
五、内部类
六、关于一些编译器的优化
在我们定义类的构造函数时可以这样子:
class date
{
public:
date(int year = 0, int month = 0, int day = 0)//构造函数
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
即使用构造函数来对对象中的成员进行赋值,虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,因为初始化只能初始化一次,而构造函数体内可以多次赋值
另外我们如果类中成员类型出现了const所修饰的成员,在构造函数体中我们无法对其进行初始化:
这是因为const是常量,我们只能在定义时将其赋值(初始化),进入构造函数时const常量已经被定义过了,我们无法在构造函数中再对其进行修改
但是我们可以在定义成员的地方给每个成员缺省值呀,比如:
class date
{
public:
date(int year = 0, int month = 0, int day = 0, int x = 0)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
const int _x = 10;//给_x一个缺省值
};
这个方法是可以,但是在C++11之前是不支持在定义处给缺省值的,所以为了方便定义类成员在构造函数中存在一个构造列表的概念:
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
例如:
class date
{
public:
date(int year = 0, int month = 0, int day = 0, int x = 0)
//初始化列表
:
_year(year),
_month(month),
_day(day),
_x(x)
{}
private:
int _year;
int _month;
int _day;
const int _x;
};
❗但是在我们使用初始化列表时要注意:
1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2. 引用成员变量、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(10)
{}
private:
A _aobj; // 没有默认构造函数
int& _ref; // 引用
const int _n; // const
};
3. 成员变量的初始化顺序是在类中声明次序,与其在初始化列表中的先后 次序关
例如:
class date
{
public:
date(int n= 0)
//初始化列表
:
_month(n),
_year(_month)
{}
void Print()
{
cout << _year << " " << _month << endl;
}
private:
int _year;
int _month;
};
int main()
{
date d1;
d1.Print();
return 0;
}
运行效果:
如上,_year成员先声明就先被初始化,而此时_month成员还未被初始化,最终造成了_year随机值的情况
我们拿下面这个类来举例:
class date
{
public:
date(int n= 0)
:
_month(n),
_year(2023)
{}
void Print()
{
cout << _year << " " << _month << endl;
}
private:
int _year;
int _month;
};
我们可以这样实例化对象(调用构造函数):
date d1(0);
但是我们还有一种方法:
date d1 = 8;//隐形类型转换
这种方法被称为隐形类型转换
int类型的常量8怎么就转换为date类型了呢?
这是因为8这个常量在编译时开辟了一个空间,这个空间自动将常量8转换为date类型的数据,再由构造拷贝函数将这个临时空间的值拷贝到d1中
但是上述过程是一些老编译器的做法,在新编译器中会直接将8作为形参传入到构造函数中,如此一来就完成了隐形类型转换
上面的构造函数只有一个参数(C++98),那多参数的构造函数怎么使用隐形类型转换呢?
使用{}将想要传入的值括起来即可(C++11):
class date
{
public:
date(int n = 0, int x = 8)
:
_month(n),
_year(x)
{}
void Print()
{
cout << _year << " " << _month << endl;
}
private:
int _year;
int _month;
};
int main()
{
date d1 = { 9,2008 };//多参数隐形类型转换
d1.Print();
return 0;
}
如果我们不想让系统进行隐形类型转换可以用到explicit这个关键字:
class date
{
public:
explicit date(int n = 0)//使用explicit关键字
:
_month(n),
_year(2023)
{}
void Print()
{
cout << _year << " " << _month << endl;
}
private:
int _year;
int _month;
};
这样当我们直接使用=传入不同类型的数据时,编译器就不会进行隐形类型转换了:
对于static这个关键字想必大家都不陌生,但是在类中的static成员有着不一样的作用,我们现在来详细说一说:
现在有一道面试题:让你统计一下一个类一共创建了多少次对象
对于这问题我们可以使用一个全局变量,每创建一个对象就在构造函数或者构造拷贝函数中对其++,但是这种方法未免太搓了,现在我们使用static成员来看看:
class A
{
public:
A()
{
++_a;
}
void Print()
{
cout << _a << endl;
}
private:
static int _a;//声明
};
int A::_a = 0;//定义
效果如下:
在上面我们创建了一个类数组a[10](即该数组有10个元素,每个元素是A类的一个对象)和一个a2对象,总计一共创建了11个对象,在每次创建对象时对其内部static成员变量_a进行++,以此来统计出一共创建了多少对象
我们可以看到我们在类中声明的静态成员变量是属于整个类的,无论我们使用哪个对象其内部static成员都是同一个_a
❗但是要注意的是:在类中的静态变量不能在声明处赋值,一定要在类外进行初始化!
对于类中的函数,我们也可以使用static来修饰:
class A
{
public:
A()
{
++_a;
}
static void Print()//使用static修饰的函数
{
//cout << _x << endl;//使用static的函数没有this指针,无法对其内部非静态成员变量进行访问
cout << _a << endl;
}
private:
int _x;
static int _a;
};
❗注意:静态成员函数没有隐藏的this指针,不能访问任何非静态成员
对于类中的静态成员函数我们可以使用类名和作用域限定符(::)来进行访问(使用创建的对象名加.也可):
我们在这里顺便插一个小知识点,我们想调用类中的函数时,如果其函数不是static类型所修饰的,我们需要创建一个对象,再用这个对象调用所需要的函数:
class A
{
public:
A()
{
++_a;
}
void Print()
{
cout << _a << endl;
}
private:
int _x;
static int _a;
};
int A::_a = 0;
int main()
{
A a;//创建一个a对象
a.Print();//使用a这个对象调用Print函数
return 0;
}
这样子十分的麻烦,不利于我们操作,现在我们来创建一个没有名字的匿名对象
对于类实例化所创建的对象我们可以不对其进行命名:
class A
{
public:
A()
{
++_a;
}
void Print()
{
cout << _a << endl;
}
~A()
{
cout << "end" << endl;
}
private:
int _x;
static int _a;
};
int main()
{
A().Print();//使用匿名对象来直接调用函数
return 0;
}
这样子我们就不需要再创建一个有名字的对象再来调用来了
不过匿名对象的生命周期在于它所在的这一行,出了该行会被自动销毁:
上面调试时,出了匿名对象该行,系统自动调用析构函数进行了销毁
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类
对于友元函数我们在类和对象(中)提到过,这里再介绍一下:
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字
具体使用方法为:friend 已定义的函数 (在类中声明)
例如:
class date
{
friend void operator<<(ostream& out, const date& d);//使用友元函数
public:
date(int year = 0, int month = 0, int day = 0)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void operator<<(ostream& out, const date& d)
{
out << d._year << "/" << d._month << "/" << d._day << endl;
}
❗注意:
友元函数可访问类的私有和保护成员,但不是类的成员函数
友元函数不能用const修饰
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数
友元函数的调用与普通函数的调用原理相同
既然被friend关键字修饰的函数可以访问一个类的私有成员,那一个类被friend关键字修饰了是不是可以访问另一个类的所有成员呢?
当然可以!
class A
{
friend class B;//B是A的友元类(即B类可以访问A类的所有成员)
public:
A()
:_x(0)
{
++_a;
}
void Print()
{
cout << _a << endl;
}
~A()
{
cout << "end" << endl;
}
private:
int _x;
static int _a;
};
int A::_a = 0;
class B
{
public:
void Print()
{
cout << _A._a << endl;//可以访问_A对象的私有成员
}
private:
A _A;
};
❗要注意:
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员
友元关系是单向的,不具有交换性(比如上述A类和B类,在A类中声明B类为其友元类,那么可以在B类中直接访问A类的私有成员变量,但想在A类中访问B类中私有的成员变量则不行)
友元关系不能传递(如果C是B的友元, B是A的友元,则不能说明C时A的友元)
友元关系不能继承,在谈到继承时再给大家详细介绍
内部类的定义:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。
内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
例如:
class A
{
public:
void APrint()
{
cout << _a <
❗注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
我们下面来看一下A类实例化的对象的大小:
这样我们很好的发现A实例化的对象是不包含B的,这很好的验证了内部类的独立性
❗下面要注意一点:内部类可以定义在外部类的public、protected、private都是可以的,但是受外部类作用限定符的约束
在上面构造函数进行隐形类型传参时,我们可以发现编译器对其进行了优化
这种情况在VS2022中还很常见,下面是一些举例:
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)
{}
A f2()
{
A aa;
return aa;
}
int main()
{
// 传值传参
A aa1;
f1(aa1);
cout << endl;
// 传值返回
f2();
cout << endl;
// 隐式类型,连续构造+拷贝构造->优化为直接构造
f1(1);
// 一个表达式中,连续构造+拷贝构造->优化为一个构造
f1(A(2));
cout << endl;
// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
A aa2 = f2();
cout << endl;
// 一个表达式中,连续拷贝构造+赋值重载->无法优化
aa1 = f2();
cout << endl;
return 0;
}
根据以上规律我们可以总结出:
1、接收返回值对象,尽量拷贝构造方式接收,不要赋值接收
2、函数中返回对象时,尽量返回匿名对象
3、尽量使用const &传参
本期博客到这里就结束了,内容较多还请各位细细观看
欢迎在评论区指出不足呀~