类与对象详解(下)
首先在这里感谢各位的大佬的阅读 如有不合理的地方 请各位大佬及时指出 蟹蟹!
内容:
1.类的6个默认成员
1.1 构造函数
1.2 析构函数
1.3 拷贝构造函数
1.4 赋值操作符重载
1.5 const成员函数
1.6 &及const&操作符重载
1.7 static成员
1.8 友元
1.9 内部类
正文
1.类的6个默认成员函数
先定义一个类
class Date
{};
如果一个类中什么成员都没有,如上代码,简称为空类。那么空类中真的什么都没有吗?其实并不是这样的,任何一个类在我们不显示定义的情况下,都会自动生成下面6个默认成员函数。
1.1构造函数
概念 :构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。
特性:
构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
主要是以下特征:
- 函数名与类名相同。
- 无返回值。
- 对象实例化是编译器自动调用对应的构造函数。
- 构造函数可以重载。
看代码
class Date
{
public:
Date()
{
cout << "Date():" << this << endl;
}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void InitDate(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void PrintDate()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1, d2, d3;//调用无参构造函数
Date d5(2019, 3, 19);//调用带参的构造函数
// Date d4(); 不是创建了一个对象,而是一个函数声明(没有参数,返回一个日期类型对象的函数声明)
d1.InitDate(2019, 3, 19);
d1.PrintDate();
d2.InitDate(2019, 3, 20);
d2.PrintDate();
d3.InitDate(2019, 3, 18);
d3.PrintDate();
return 0;
}
-
如果类中没有显式定义构造函数,C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义,编译器将不再生成。默认的构造函数一定是无参的。
-
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。**注意:**无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
class Date
{
public:
// 默认构造函数
// 无参构造函数
// Date()
// {
// cout << "Date():" << this << endl;
// }
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void PrintDate()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
// 默认的构造函数只能存在一个
Date d1;
//Date d2(2019, 3, 19);
return 0;
}
如果这里将无参构造函数放开,运行到 Date d1时编译器会报错。因为这里的 Date d1 不知道要调用哪一个构造函数。
- . 关于编译器生成的默认成员函数,很多童鞋会有疑惑:在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?
其实并不然,编译器会在感觉自己需要的时候才会生成默认的构造函数。
比如如下场景:
class Time
{
public:
Time(int hour = 0, int minute = 0, int second = 0)
{
cout << this->_hour << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
//编译器自动生成默认构造函数
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
Date d; // 必须调用Time() --->找一个调用位置--->日期类的构造函数
return 0;
}
上面的场景中,要运行Date d;语句,编译器一定会生成Date类的默认构造函数。
构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
8.初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
看代码:
class Time
{
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
注意:
-
每个成员变量在初始化列表中只能出现一次。
-
类中包含以下成员,必须放在初始化列表位置进行初始化:
引用成员变量
const成员变量
类类型成员(该类没有默认的构造函数) -
尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
-
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。(最好初始化顺序与声明顺序相同)
9 .explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用。
class Date
{
public:
explicit Date(int year)
: _year(year)
{
cout << "Date(int,int,int):" << this << endl;
}
Date& operator=(const Date& d) //赋值运算符重载 将后面进行详细说明
{
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d(2019);
d = 2020; // 2020---> 通过单参构造函数--->临时对象
return 0;
}
用explicit修饰构造函数,将会禁止单参构造函数的隐式转换。
1.2 析构函数
- 概念:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
- 特性:
- 析构函数名是在类名前加上字符~。
- 无参数无返回值。
- 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
- 对象声明周期结束时,C++编译系统自动调用析构函数。
- 当类里面涉及资源管理时,用户必须显式定义析构函数,防止内存泄漏。
class SeqList
{
public:
SeqList(int capacity = 10)
{
cout << "SeqList(int):" << this << endl;
_array = (int*)malloc(capacity*sizeof(int));
assert(_array);
_capacity = capacity;
_size = 0;
}
/*
~SeqList() //默认生成的析构函数
{}
*/
~SeqList()
{
//cout << this << endl;
if (_array)
{
free(_array);
_capacity = 0;
_size = 0;
}
cout << "~SeqList():" << this << endl;
}
private:
int* _array;
int _capacity;
int _size;
};
void TestSeqList()
{
SeqList s;
}
int main()
{
TestSeqList();
return 0;
}
- 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的
默认析构函数,对会自定类型成员调用它的析构函数。
class String
{
public:
String(const char* str = "")
{
cout << "String(const char* ):" << this << endl;
if (nullptr == str)
str = "";
_str = (char*)malloc(strlen(str)+1);
strcpy(_str, str);
}
~String()
{
cout << "~String():" << this << endl;
free(_str);
_str = nullptr;
}
private:
char* _str;
};
class Person
{
private:
String _name;
String _gender;
int _age;
};
void TestPerson()
{
Person p; //
}
int main()
{
TestPerson();
return 0;
}
在Person类中:
默认的构造函数—将对象中_name和_gender两个String类型的对象构造好。
默认的析构函数—将_name和_gender两个String类的对象销毁掉
1.3 拷贝构造函数
- 概念: 只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
- 特征:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用(调用构造函数)。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
cout << "Date(int,int,int):" << this << endl;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
cout << "Date(Date&):" << this << endl;
}
~Date()
{
cout << "~Date():" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
Date d1(2019, 3, 22);
Date d2(d1);
}
int main()
{
TestDate();
return 0;
}
- 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。
当类中涉及资源管理时,浅拷贝就会引发问题
class String
{
public:
String(const char* str = "")
{
cout << "String(const char* ):" << this << endl;
if (nullptr == str)
str = "";
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
// 默认生成拷贝构造函数
~String()
{
cout << "~String():" << this << endl;
free(_str);
_str = nullptr;
}
private:
char* _str;
};
void TestString()
{
String s1("hello");
String s2(s1);
}
int main()
{
TestString();
system("pause");
return 0;
}
运行结果:
程序出错的原因:
在程序结束时,要对构造的对象就行析构,因为s2是s1的浅拷贝,所有把s2析构之后,s1也被干掉了,但是系统不知道,还会去调用s1的析构函数,所以程序才会崩溃。
所以当类中涉及资源管理时,拷贝构造函数应该显式提供。
1.4 赋值操作符重载
1.41 运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
- 不能通过连击其他符号来创建新的操作符。
- 重载操作符必须有一个类类型或者枚举类型的操作数。
- 用于内置类型的操作符,其含义最好不要改变。
- 作为类成员的重载函数时,其形参看起来比操作数数目少1,因为成员函数的操作符有一个默认的形参this,限定为第一个形参
- .* 、::、sizeof 、?:、. 以上5个运算符不能重载。
列举一些运算符重载的实现:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
cout << "Date(int,int,int):" << this << endl;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
cout << "Date(Date&):" << this << endl;
}
// *this + day
Date operator+(int day)
{
Date temp(*this);
temp._day += day;
return temp;
}
Date& DateAdd(int day)
{
_day += day;
return *this;
}
//重载 ==
bool operator==(const Date& d)
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
//重载 !=
bool operator!=(const Date& d)
{
return !(*this == d);
}
// 重载前置++
Date& operator++()
{
_day += 1;
return *this;
}
// 重载后置++
Date operator++(int)
{
Date temp(*this);
_day += 1;
return temp;
}
//重载前置 --
Date& operator--()
{
_day -= 1;
return *this;
}
//重载后置--
Date operator--(int)
{
Date temp(*this);
_day -= 1;
return temp;
}
~Date()
{
cout << "~Date():" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
Date d1(2019, 3, 22);
d1.DateAdd(3);
d1 = d1 + 3;
Date d2(d1);
d2 = d1++;
d2 = ++d1;
Date d3(2018, 3, 22);
d3 = d3;
d3 = d1;
//d3.operator=(d1);
d1 = d2 = d3;
//d1.operator=(d2.operator=(d3));
if (d3 == d1);
Date& d4 = d3;
d4 = d3;
}
int main()
{
TestDate();
return 0;
}
1.42 赋值运算符重载
class Date
{
public :
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date (const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date& operator=(const Date& d)
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
}
private:
int _year ;
int _month ;
int _day ;
};
赋值运算符重载特点
- 参数类型。
- 返回值。
- 检测是否自己给自己赋值。
- 返回*this
- 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。(浅拷贝)
1.5 const成员
1.51 const修饰类的成员函数
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
class Date
{
public:
void TestFunc2()const
{
this->_day++;
//_year++;
//_month++;
}
void TestFunc2()
{
this->_day++;
//_year++;
//_month++;
}
private:
int _year;
int _month;
mutable int _day;
};
int main()
{
Date d1;
d1.TestFunc2();
const Date d2;
d2.TestFunc2();
system("pause");
return 0;
}
d1.TestFunc2(); 语句将会调用没有const修饰的TestFunc2函数;
d2.TestFunc2();语句将会调用有const修饰的TestFunc2函数;
1.6 取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义,编译器默认会生成。如下代码
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
1.7 static成员
概念: 声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态的成员变量一定要在类外进行初始化。
特性:
- 静态成员为所有类对象共享,不属于某个具体实例。
- 静态成员变量必须在类外进行定义,定义时不添加static关键字。
- 类静态成员即可用类名::静态成员或者对象.静态成员来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值。
面试题: 实现一个类,计算程序中创建出了多少类对象。
class A
{
public:
A()
{
++_count;
}
A(const A& t)
{
++_count;
}
static int GetCount()
{
return _count;
}
private:
static int _count;
};
int A::_count = 0;
void Test()
{
cout << A::GetCount() << endl;// 0
A a1, a2;
A a3(a1);
cout << A::GetCount() << endl;//3
}
int main()
{
Test();
system("pause");
return 0;
}
输出结果: 0 和 3;
1.8 友元
1.81 友元函数
问题:现在我们尝试去重载operator<<,然后发现我们没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以我们要将operator<<重载成全局函数。但是这样的话,又会导致类外没办法访问成员,那么这里就需要友元来解决。operator>>同理。
重载 operator<<
条件:
- 两个参数:参数一 一定为ostream&, 参数2输出的内容
- 必须要有返回值:ostream&, 支持连续输出
- 少做格式化操作:比如换行
- 将该函数作为类的友元函数
class Date
{
// 友元函数
friend ostream& operator<<(ostream& _cout, const Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{
cout << "Date(int,int,int):" << this << endl;
}
void PrintDate()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
// _cout<<d.GetYear()<<"-"<<d.GetMonth()<<"-"<<d.GetDay();
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
int main()
{
Date d(2019, 3, 24);
d.PrintDate();
cout << 10 << endl;
// cout<<10 cout<<endl;
cout << d << endl;
//cout << 10;
//cout << d;
//d.operator<<(cout);
//d << cout;
system("pause");
return 0;
}
运行结果:
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,bu不属于任何类,但需要在类的内部声明,声明是需要加friend关键字
注意:
- 友元函数可以访问类的私有成员,但不是类的成员函数。
- 友元函数不能用const修饰。
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
- 一个函数可以是多个类的友元函数。
- 友元函数的调用和普通函数的调用原理相同。
1.82 友元类
class Date
{
friend class Time;//声明时间类为日期类的友元类,则在时间类中就可以直接访问日期类中的私有成员变量
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{
cout << "Date(int,int,int):" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
class Time
{
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{
cout << this->_hour << endl;
}
void TestFriendClass()
{
Date d;
}
private:
int _hour;
int _minute;
int _second;
};
void Test()
{
Time t;
t.TestFriendClass();
}
int main()
{
Test();
system("pause");
return 0;
}
声明时间类为日期类的友元类,则在时间类中就可以直接访问日期类中的私有成员变量
注意:
- 友元关系是单向的,不具有交换性。
- 友元关系不能传递
即:如果B是A的友元,C是B的友元,不能说明C是A的友元。
1.9 内部类
概念: 如果一个类定义在另一个类的内部,这个内部类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对部类没有任何优越的访问权限。
注意: 内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的
所有成员。但是外部类不是内部类的友元。
特性:
- 内部类可以定义在外部类的任意位置。
- 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象或类名。
- sizeof(外部类) = 外部类,和内部类没有任何关系。
谢谢大家的阅读!!