文章目录
再谈构造函数
在构造函数体内赋值
在创建对象时,编译器自动调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称作为类对象成员的初始化,构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
为什么下面的代码错误?
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
_N = 20;
//报错,error C2166:左值指定const对象
//这里报错说明到构造函数体内时,成员变量已经定义出来了,成员变量在哪里定义的呢?--初始化列表
}
private:
int _year;//成员变量的声明
int _month;
int _day;
const int _N;
};
int main()
{
Date d1(2022,2,19);//对象定义/对象实例化
return 0;
}
构造函数实际包含两步:1.先在初始化列表中定义非静态成员变量,2.在构造函数体内进行赋值操作,理解这个很重要!
初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个括号,放在括号中的是初始值或表达式。只有构造函数(拷贝构造也是构造函数的一种)中可以使用初始化列表。
class Date
{
public:
//构造函数
Date(int year, int month, int day) :_year(year), _month(month), _day(day)
{
}
private:
int _year;//成员变量的声明
int _month;
int _day;
};
初始化列表是非静态成员变量定义的地方,对于成员变量可以在定义的时候就进行初始化,也可以先定义后面再进行初始化,当然有些类型的成员变量除外。
类的定义是不占用空间的(存放在代码段),只有实例化时候才会为该对象开辟空间,空间中存储对象的成员数据/成员变量(属性)。
【注意】
-
每个成员变量在初始化列表中最多只能出现一次(因为初始化只能初始化一次,当然有些成员变量也可以不出现)
-
类中包含以下成员时,必须放在初始化列表位置进行初始化,因为他们都必须在定义时进行初始化,而初始化列表就是成员变量定义的地方:
- 引用成员变量
- const成员变量
- 自定义类型成员(如果该类没有默认构造函数)
类中包含这三种类型成员变量时,有下面两种写法:
(1)所有成员变量都在初始化列表中进行初始化
class A
{
public:
//不是默认构造函数
A(int a)
{
_a = a;
}
private:
int _a;
};
class Date
{
public:
//初始化列表是成员变量定义的地方
Date(int year, int month, int day, int i, A aa)
:_year(year), //定义_year同时给初始值year
_month(month),
_day(day),
_N(10),
ref(i),
_aa(aa)
{
}
private:
int _year;//成员变量的声明
int _month;
int _day;
const int _N;//声明
int& ref;
A _aa;
};
int main()
{
A a(10);
Date d1(2022,2,19,10,a);//对象定义/对象实例化
return 0;
}
(2)对于_year,_month,_day可以不在初始化列表中进行初始化,先在初始化列表中定义,后面在函数体内再进行赋值操作也是可以的。
class Date
{
public:
//初始化列表是成员变量定义的地方
Date(int year, int month, int day, int i, A aa) //定义所有成员变量
:_N(10),//定义同时进行初始化
ref(i),
_aa(aa)
{
_year = year, //赋值
_month = month,
_day = day,
}
private:
int _year;//这里是成员变量的声明,实例化时才会为对象开空间,存储成员变量的值
int _month;
int _day;
const int _N;
int& ref;
A _aa;
};
int main()
{
A a(10);
Date d1(2022,2,19,10,a);//对象定义/对象实例化
//可以使用匿名对象
Date d2(2022,2,20,20,A(20));
return 0;
}
我们推荐(1)的写法。
- 尽量使用初始化列表初始化,因为使用初始化列表一定不会出问题,但是如果在构造函数体内赋值,对于自定义类型变量可能会出问题。
对比上面两段代码,在初始化列表中进行初始化,会调用一次拷贝构造;在函数体赋值,会调用一次构造函数进行初始化(在初始化列表中定义),然后调用一次赋值重载(在函数体内赋值)。
对于内置类型,在函数体内赋值和初始化列表中初始化都是可以的;
对于自定义类型,建议在初始化列表中进行初始化,这样更高效,对比上面的例子得出。
- 成员变量在类中声明的次序就是其在初始化列表中的初始化顺序,与其在初始化列表中写的先后次序无关。
下面这段代码的结果是什么?
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. 输出1 1
B.程序崩溃
C.编译不通过
D.输出1 随机值
答案是D。在类中,先声明_a2,后声明_a1,所以在初始化列表中先初始化_a2,后初始化_a1(在初始化之前成员变量都定义好了,定义其实是按照对象的大小开好一块空间,初始化是向空间里面填值)。
构造函数和初始化列表的区别
构造函数,其实有默认生成的初始化列表,初始化列表定义所有的成员变量,然后在函数体内对成员变量进行初始化操作;
初始化列表是在定义成员变量的同时就对其进行初始化,并且对初始化列表中成员变量的定义、初始化工作是先于构造函数体内代码的;
因为对于有些类型的变量,必须在定义时候就得进行初始化操作,所以只能在初始化列表中进行初始化(比如const常变量、引用类型、没有默认构造函数的自定义类型对象)。
explicit关键字
首先回顾一下隐式类型转换
int main()
{
double d = 3.14;
int i = d;//隐式类型转换 -- 相近类型,表示意义相似的类型
//隐式类型转换会产生临时变量,临时变量具有const常属性
int* p = &i;
int j = (int)p;//强制类型转换 -- 无关类型
return 0;
}
C++的构造函数不仅可以构造和初始化对象,对于单个参数的构造函数,还具有隐式类型转换的作用。
class Date
{
public:
Date(int year)
:_year(year)
{
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2020);
// 用一个整形变量给日期类型对象赋值
// 实际编译器背后会用2021构造一个无名对象,最后用无名对象给d1对象进行赋值
d1 = 2021;
Date d2 = 2022
//这里用一个整形变量给日期类型对象赋值 ,会有隐式类型转换,
//本来用2022构造一个临时对象Date(2022),再用这个对象拷贝构造d2,
//但是c++编译器在连续的一个过程中,多个拷贝构造会被优化,合二为一,
//所以这里被优化成直接就是一个构造。
}
但是,如果我们不想让这样情况发生,怎么办?C++提供一个关键字explicit。用explicit修饰构造函数,将会禁止单参数构造函数的隐式转换,下面的代码报错
class Date
{
public:
explicit Date(int year)
:_year(year)
{
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2020);
// 用一个整形变量给日期类型对象赋值
// 实际编译器背后会用2021构造一个无名对象,最后用无名对象给d1对象进行赋值
d1 = 2021;
}
static成员
假如我们现在想计算某一个类实例化出来的对象的个数,该怎么办?
我们定义一个全局变量count,每次调用构造函数或者拷贝构造的时候,count++,这样就可以计算对象个数了。
int count;
class A
{
public:
A(int a = 10) :_a(a)
{
count++;
}
A(const A& a):_a(a._a)
{
count++;
}
private:
int _a;
};
int main()
{
A a1;
A a2;
std::cout<< count <<std::endl;
count++;
return 0;
}
但是有一个问题,在类的外面,我们是可以改变这个变量,那么就没有达到我们的目的。假如把count作为类的成员变量
class A
{
public:
A(int a = 10) :_a(a)
{
count++;
}
A(const A& a):_a(a._a)
{
count++;
}
private:
int _a;
int count;
};
int main()
{
A a1;
A a2;
return 0;
}
但是这样的话,每个对象中都存储了一份自己的count变量,那么该怎么办呢?C++的类有一个static成员可以解决这个问题。
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。
在类里面没有地方能定义静态成员变量,所以静态的成员变量一定要在类外进行定义,即使这个静态成员变量是私有的。
class A
{
public:
A(int a = 10) :_a(a)
{
_sCount++;
}
A(const A& a):_a(a._a)
{
_sCount++;
}
//static int
//静态成员函数:没有this指针,只能访问静态成员变量和静态成员函数
static int GetCount()
{
return _sCount;
}
private:
int _a;
//静态成员变量属于整个类,并不是单单属于某个对象,属于所有的对象,生命周期在整个程序运行期间
static int _sCount;//声明,私有的,只能在类里面访问
};
int A::_sCount = 0;//静态成员变量的定义并进行初始化
int main()
{
A a1;
A a2;
//如何在类外面访问_sCount?
//如果_sCount是公有的,可以这样访问:A::_sCount 或者 对象名._sCount
//但是现在_sCount是私有的,怎么访问? -- 使用静态成员函数
//两种调用方式
std::cout<<a1.GetCount()<<std::endl;
std::cout<<A::GetCount()<<std::endl;
return 0;
}
练习:求1+2+3+…+n,要求不能使用乘除法、if、else、for、while、switch、case关键字.
class Sum
{
public:
Sum()
{
_i++;
_ret += _i;
}
static int GetCount()
{
return _ret;
}
private:
static int _i;
static int _ret;
};
int Sum::_i = 0;
int Sum::_ret = 0;//存放和
class Solution
{
public:
int Sum_Solution(int n)
{
//Sum a[n];//变长数组,如果不支持,使用new
Sum* p = new Sum[n];//实例化n个Sum类的对象
delete[] p;
return Sum::GetCount();
}
};
int main()
{
Solution s;
int ret = s.Sum_Solution(10);
std::cout << ret << std::endl;
return 0;
}
静态成员的特性
-
静态成员为所有类对象所共享,不属于某个具体的实例;
-
静态成员变量必须在类外定义,定义时不添加static关键字,初始化列表是定义非静态成员变量的地方(构造函数不写初始化列表,也会有一个默认的初始化列表来定义非静态成员变量),类中没有定义静态成员变量的地方,只能在类外面进行定义和初始化,在类内部只能获取和赋值操作;
-
类静态成员即可用类名::静态成员或者对象.静态成员来访问;
-
静态成员函数没有隐藏的this指针,不能访问任何非静态成员;
-
静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值。
【问题】
- 静态成员函数可以调用非静态成员函数吗?不可以
- 非静态成员函数可以调用类的静态成员函数吗?可以
C++11的成员初始化
我们“类和对象(二)”这一章节中知道,C++98中,对于类而言,默认生成的构造函数对于内置类型成员变量不做处理,对于自定义类型成员变量会去调用它的默认构造函数,没有对内置类型和自定义类型做出统一的处理,这也是C++的构造函数为人诟病的地方。
class B
{
public:
};
class A
{
public:
private:
int _a1;
B _bb;
};
int main()
{
A aa;
return 0;
}
调试代码发现,对于内置类型_a1并没有进行初始化,_a1就是一个随机值。
那么在C++11中,为了给内置类型成员变量初始化,并且还要兼容以前版本,就打了一个补丁。C++11支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量缺省值。这个缺省值的作用是:当初始化列表没有对非静态成员变量初始化时,用这个缺省值来初始化。
看下图中的代码,在类中声明成员变量时,给一个缺省值,在构造函数初始化列表中不对成员变量进行初始化,那么就会使用缺省值来进行初始化,调试代码发现,aa._a1的值是0。
同时,也可以给自定义类型成员变量缺省值。
class B
{
public:
B(int b):_b(b)
{
}
private:
int _b;
};
//C++打补丁
class A
{
public:
A(int a1):_a1(a1)
{
}
private:
//注意:这里不是初始化,这里放的都是声明
int _a1 = 0;//给成员变量缺省值
//给自定义类型变量缺省值
B _bb = 10;//隐式类型转换
B _bb2 = B(20);//匿名对象来初始化_bb2
int* p = (int*)malloc(4*20);
int arr[10] = {
1,2,3,4,5};
//static int count = 0;//错误
};
int main()
{
A aa(10);
return 0;
}
注意:静态成员变量不能在声明时给缺省值,静态成员变量的定义和初始化只能在类外面。因为非静态成员变量给缺省值,是当初始化列表中不显式对他们进行初始化时,用这个缺省值给他们初始化,但是静态成员变量的定义和初始化不能在类中进行,所以静态成员变量就不能有缺省值。
友元
//Date.h
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 0, int month = 0, int day = 0);
void operator<<(ostream& out);
private:
int _year;
int _month;
int _day;
};
//Date.cpp
#include "Date.h"
//运算符重载里面,如果是双操作数的操作符重载,第一个参数是左操作数,第二个参数是右操作数
void Date::operator<<(ostream& out)
{
out << _year << "/" << _month << "/" << _day << endl;
}
//test.cpp
#include "Date.h"
int main()
{
Date d1(2022,2,17);
//cout << d1 ;//没办法使用,需要我们自己重载<<
//重载后,仍没法cout<<d1这样使用
d1.operator<<(cout);
d1 << cout;//可以,但是用起来很奇怪
//所以,<<没办法重载
//怎么解决?---定义全局函数
//cin >> d1;
return 0;
}
C++对于自定义对象,没有默认重载流提取运算符<<,我们必须自己写,在运算符重载里面,如果是双操作数的操作符重载,第一个参数是左操作数,第二个参数是右操作数,所以上面的类成员函数operator<<的第一个隐藏的this指针是第一个操作数,所以只能d1<<cout这样使用,但是这样使用右比较怪异,那怎么办呢?我们可以在类外面定义一个函数来实现。这里就不得不提到友元这个概念。
什么是友元?
友元分为:友元函数和友元类。
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用(能不用就不用)。
友元函数
回顾上面的问题:现在我们尝试去重载operator<<,然后发现我们没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以我们要将operator<<重载成全局函数。但是这样的话,又会导致类外没办法访问成员,那么这里就需要友元来解决。operator>>同理。
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声
明,声明时需要加friend关键字。
//Date.h
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 0, int month = 0, int day = 0);
//声明友元函数
friend void operator<<(ostream& out, const Date& d);
private:
int _year;
int _month;
int _day;
};
//Date.cpp
#include "Date.h"
//友元函数实现---类外面的普通函数,定义不需要使用friend
void operator<<(ostream& out, const Date& d)
{
//一个全局函数中想访问对象的私有成员变量 - 使用友元
out << d._year << "/" << d._month << "/" << d._day << endl;
}
//test.cpp
#include "Date.h"
int main()
{
Date d1(2022,2,17);
cout<<d1;//可以正常使用
return 0;
}
如何实现cout<<d1<<d2;呢?将函数返回值类型修改一下即可
friend ostream& operator<<(ostream& out, const Date& d);
ostream& operator<<(ostream& out, const Date& d)
{
//一个全局函数中想访问对象的私有成员变量 - 使用友元
out << d._year << "/" << d._month << "/" << d._day << endl;
return out;
}
同理,实现cin重载
//Date.h
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 0, int month = 0, int day = 0);
//声明友元函数
friend ostream& out operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
private:
int _year;
int _month;
int _day;
};
istream& operator>>(istream& in, Date& d)
{
cout << "请一次输入年月日:";
in >> d._year >> d._month >> d._day;
return in;
}
注意:
-
友元函数可访问类的私有和保护成员,但它不是类的成员函数(就是一个普通的全局函数)
-
友元函数不能用const修饰(const修饰的是非静态成员函数,真实修饰的是非静态函数的第一个隐藏参数this指针,表示this指针指向的对象不能修改)
-
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
-
一个函数可以是多个类的友元函数
-
友元函数的调用与普通函数的调用和原理相同
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
-
友元关系是单向的,不具有交换性。
比如Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。(我是你的粉丝,我可以看到你的动态,并不代表你也是我的粉丝,你也能看我的动态;你想看我的动态,你要先成为我的粉丝,互关之后,我们才都是彼此的粉丝,都能看对方的动态)。 -
友元关系不能传递
如果B是A的友元,C是B的友元,则不能说明C是A的友元。
class Date; // 前置声明
class Time
{
friend class Date; // 声明日期类为时间类的友元类,
//则在日期类中就直接访问Time类中的私有成员变量
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{
}
void f(Date d);
private:
int _hour;
int _minute;
int _second;
};
class Date
{
friend class Time;//Time类是Date类的友元
public:
Date(int year = 2022, int month = 1, int day = 1)
: _year(year),
_month(month),
_day(day)//没有显式给自定义成员变量_t初始化,那么就会调用它的默认构造函数
{
_t._hour = 0;
_t._minute = 0;
_t._second = 0;
}
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;
};
void Time::f(Date d)
{
std::cout << d._year << "-" << d._month << "-" << d._day << std::endl;
}
内部类
如果一个类定义在另一个类的内部,这个类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
- 内部类定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
- sizeof(外部类)=外部类,和内部类没有任何关系。
如果定义一个类,不想让别人用,就把它定义成内部类,并且是外部类的private。
//A是外部类/全局类
class A
{
private:
static int k;
int h;
public:
//B是内部类
//1、内部类B和在全局定义基本是一样的,只是它受外部类A类域限制
//2、内部类B天生就是外部类A的友元,也就是B中可以访问A的私有或者保护,
//但是A不能访问B的私有或者保护
//A如果想访问B的私有,要在B中声明A为B的友元
class B
{
public:
friend class A;//声明A为B的友元
void foo(const A& a)
{
std::cout << k << std::endl;//OK
std::cout << a.h <<std::endl;//OK
}
private:
int _b;
};
void f(B b)
{
//A为B的友元之后,才可以访问B的私有成员
std::cout << b._b << std::endl;
}
};
int A::k = 1;
int main()
{
//B b;//错误
A::B b;
b.foo(A());//匿名对象A()
std::cout << sizeof(A) << std::endl;//4
return 0;
}
对于之前的联系:求1+2+3+…+n,要求不能使用乘除法、if、else、for、while、switch、case关键字.我们可以使用内部类来修改一下。
class Solution
{
private:
//内部类
class Sum
{
public:
Sum()
{
_i++;
_ret += _i;
}
static int GetCount()
{
return _ret;
}
private:
static int _i;
static int _ret;
};
public:
int Sum_Solution(int n)
{
//Sum a[n];//变长数组,如果不支持,使用new
Sum* p = new Sum[n];//实例化n个Sum类的对象
delete[] p;
return Sum::GetCount();
}
};
int Solution::Sum::_i = 0;
int Solution::Sum::_ret = 0;//存放和
int main()
{
Solution s;
int ret = s.Sum_Solution(10);
std::cout << ret << std::endl;
return 0;
}
可以不使用静态成员函数,将_ret定义为外部类的私有成员,内部类B是外部类A的友元,可以直接访问外部类的私有成员。
class Solution
{
private:
//内部类
class Sum
{
public:
Sum()
{
_i++;
_ret += _i;
}
private:
static int _i;
};
public:
int Sum_Solution(int n)
{
//Sum a[n];//变长数组,如果不支持,使用new
Sum* p = new Sum[n];//实例化n个Sum类的对象
delete[] p;
return _ret;
}
private:
static int _ret;
};
int Solution::Sum::_i = 0;
int Solution::_ret = 0;//存放和
int main()
{
Solution s;
int ret = s.Sum_Solution(10);
std::cout << ret << std::endl;
return 0;
}
但是C++很少使用内部类。
关于C++面向对象其他内容,我们在C++高阶中再学习。