我们上篇文章介绍了C++的很多基本知识,这篇文章是对上篇文章的补充,并且还会介绍很多有趣的知识,希望大家能耐心阅读下去,一定会有收获的。
重新谈论构造函数
实例化对象时会通过调用构造函数,给对象的成员变量赋值:
class Date
{
public:
Date(int year =2024, int month = 5, int day = 5)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
但是实际上我们上面调用了构造函数的操作,其实不能称之为初始化,只能被称之为赋初值,因为初始化只有一次,但是赋值可以赋很多次,如下面的代码,_year就被赋值了3次:
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = 1;
_year = 2;
_year = year;//这里的_year就被赋值了三次
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
初始化列表
我们刚刚讲调用上面的构造函数并不是初始化,只是对成员变量赋值而已,那么要怎么给成员变量初始化呢?答案就是用初始化列表。
初始化列表:以冒号开头,后跟一系列以逗号分隔的初始化字段,每个成员变量后面跟一个放在括号中的初始值或表达式。如:
class Date
{
public:
Date(int year = 2024, int month = 5, int day = 5)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
初始化列表的注意事项:
一.因为是初始化,所以每个成员变量在初始化列表中只能出现一次
二.类中包含以下成员的话,必须放在初始化列表中进行初始化:
1.引用成员变量,因为引用类型的变量在定义时就必须给一个初始值
2.const成员变量,因为const成员变量也必须在定义的时候给一个初始值
3.自定义类型成员变量(并且这个类没有默认构造函数)
如果一个类没有默认构造函数,那么我们在实例化该类对象时就需要传参对其进行初始化,所以对没有默认构造函数的类的成员也必须使用初始化列表对其初始化。
我们通过下面的例子加深一下理解:
class A
{
public:
A(int a)//A没有默认构造函数
{
_a = a;
}
private:
int _a;
};
class B
{
public:
B(int a,int b,A c)://上述三种成员变量必须通过初始化列表初始化
_a(a)
,_b(b)
,_c(c)
{}
//B(int a, int b, A c)//这是错误的示例
//{
// _a = a;
// _b = b;
// _c = c;
//}
private:
int& _a;
const int _b;
A _c;
};
三、尽量都使用初始化列表初始化
初始化列表实际上是我们实例化一个对象时,该对象的成员变量定义的地方,所以无论是否使用初始化列表,都会走这样一个过程。
实际上,对于内置类型,使用初始化列表和在构造函数内进行初始化本质上没有差别,其差别就类似于下面的代码:
int x = 10;//初始化列表
int x;//在构造函数中初始化
x = 10;
但对于自定义类型,使用初始化列表可以提高代码效率:
class Time
{
public:
Time(int hour = 0)
{
_hour = hour;
}
private:
int _hour;
};
class Date
{
public:
Date(int hour)
:_t(12)// 使用初始化列表,只用调用一次Time类的构造函数
{}
private:
Time _t;
};
但是若我们不使用初始化列表,想要达到相同的效果,我们就得这样写:
class Time
{
public:
Time(int hour = 0)
{
_hour = hour;
}
private:
int _hour;
};
class Date
{
public:
Date(int hour)
{ //初始化列表调用一次Time类的构造函数(不使用初始化列表但也会走这个过程)
Time t(hour);// 调用一次Time类的构造函数
_t = t;// 调用一次Time类的赋值运算符重载函数
}
private:
Time _t;
};
对于自定义类型成员变量,尽管我们没有显示的写初始化列表,这里也是认为有初始化列表的,所以自定义变量一定会使用初始化列表初始化。所以我们建议:能使用初始化列表初始化的成员尽量使用初始化列表初始化,因为就算你不显示使用初始化,成员也会先用初始化列表初始化一遍。
四、成员变量在类中初始化的次序就是其在类中声明顺序,与其在初始化列表中的顺序无关。例如:
#include <iostream>
using namespace std;
int i = 0;
class Test
{
public:
Test()
:_b(i++)
,_a(i++)
{}
void Print()
{
cout << _a << endl;
cout << _b << endl;
}
private:
int _a;
int _b;
};
int main()
{
Test test;
test.Print(); //结果为0,1,说明初始化的顺序是_a先初始化,_b后初始化
return 0;
}
explicit关键字
构造函数不仅可以构造和初始化对象,对于单个参数的构造函数,还支持隐式类型转换。
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 0) //单个参数的构造函数
:_year(year)
{}
void Print()
{
cout << _year << endl;
}
private:
int _year;
};
int main()
{
Date d = 2024;
d.Print();
return 0;
}
代码Date d =2024其实等价于下面两句代码:
Date tmp(2024);
Date d1(tmp);
在早期的编译器中,在遇到Date d =2024这句代码时,会先创造一个临时对象,再用这个临时对象拷贝构造d;但是现在编译器已经做了优化,当遇到这样的代码时,会当成Date d(2024);,这就叫做隐式类型转换。
对于Date d =2024这种代码他的可读性并不是很好,若我们想禁止单参数构造函数的隐式转换,可以使用关键字explicit来修饰构造函数。
static成员
声明为static的类成员称为类的静态成员。用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。
特性
一、静态成员被所有类对象共享,不属于某个具体的对象
我们举个例子:
#include <iostream>
using namespace std;
class Test
{
private:
static int x;
};
int main()
{
cout << sizeof(Test) << endl;
return 0;
}
上面代码的运行结果为1,这是因为静态成员x是存储在静态区的,属于整个类,也属于类的所有对象。所以计算类的大小时,静态成员并不算入总大小以内。
二、静态成员变量必须在类外定义,定义时不添加static关键字
class Test
{
private:
static int _n;
};
int Test::_n = 0;
我们会发现一个特殊的现象:_n是私有成员,但是我们在类外进行了访问。这是一个特例,static成员不受限定符的限制,否则就没法对静态成员变量进行定义和初始化了。
三、静态成员函数没有this指针,不能访问任何非静态成员
class Test
{
public:
static void Fun()
{
cout << _a << endl; //错误,静态成员函数不能访问非静态成员
cout << _n << endl; //正确
}
private:
int _a; //非静态成员
static int _n; //静态成员
};
因此,含有静态成员变量的类,一般都会含有一个静态成员函数用来访问静态成员变量。
四、访问静态成员变量的方法
1.当静态成员变量为共有时,有以下几种访问方式:
#include <iostream>
using namespace std;
class Test
{
public:
static int _n; //静态成员变量
};
int Test::_n = 0;//静态成员变量初始化
int main()
{
Test test;
cout << test._n << endl; //1.通过类对象突破类域进行访问
cout << Test()._n << endl; //3.通过匿名对象突破类域进行访问
cout << Test::_n << endl; //2.通过类名突破类域进行访问
return 0;
}
2.当静态成员变量为私有时,有以下几种访问方式:
#include <iostream>
using namespace std;
class Test
{
public:
static int GetData()
{
return _n;
}
private:
static int _n;
};
// 静态成员变量的定义初始化
int Test::_n = 0;
int main()
{
Test test;
cout << test.GetDAta() << endl; //1.通过对象调用成员函数进行访问
cout << Test().GetData() << endl; //2.通过匿名对象调用成员函数进行访问
cout << Test::GetData() << endl; //3.通过类名调用静态成员函数进行访问
return 0;
}
五、静态成员和类的普通成员一样,也有public,private,protected三种限定。当我们把静态成员设置为非public时,尽管我们突破了类域,也不能对其进行访问。
我们需要注意两个问题:
1.静态成员函数可以调用非静态成员函数吗?
不可以,因为非静态成员函数的第一个形参默认是this指针,而静态成员函数中没有this指针,故静态成员函数不可以调用非静态成员函数。
2.非静态成员函数可以调用静态成员函数吗?
可以,还是因为非静态成员函数有this指针,静态成员函数没有this指针。
C++11中成员初始化的新特性
C++支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量一个缺省值。如下面的代码:
class A
{
public:
void Print()
{
cout << _a << endl;
cout << _p << endl;
}
private:
int _a = 1; // 非静态成员变量,可以在成员声明时给缺省值。
int* _p = new int(5);
static int _n; //静态成员变量不能给缺省值
};
初始化列表是成员变量初始化的地方,你若是给定了值,就用你给的值对成员变量进行初始化,你若是没有给定值,就用缺省值进行初始化,若还没有缺省值,内置类型的成员就是随机值。
友元
友元分为友元函数和友元类。友元的作用是突破封装,有时候提供了便利,但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
如我们想要设计一个日期类,尝试重载operator<<和operator>>的时候,但是我们会发现没法将其重载为成员函数因为cout和cin的输出流对象和隐含的this指针在抢占第一个参数的位置:this默认是第一个参数,即左操作数,但是实际使用中cout和cin需要的是第一个形参对象才能正常使用。
所以我们要将operator<<和>>设置为全局函数,但是这样的话,又会导致类外没办法访问成员,所以这里就需要友元来解决。
补充:C++中的cin和cout都可以自动识别输入和输出变量类型,我们不必再像使用printf时增加格式控制。实际上,这是因为库里面已经将<<和>>重载好了,可以自动识别内置类型。
若我们想让<<和>>也自动识别我们的日期类,就需要我们自己写出对应的运算符重载函数。
class Date
{
friend istream& operator>> (istream& in, Date& d);
friend ostream& operator<< (ostream& out, const Date& d);
public:
Date(int year,int month,int day):
year_(year),
month_(month),
day_(day)
{}
private:
int year_;
int month_;
int day_;
};
istream& operator>> (istream& in, Date& d1)
{
in >> d1.year_ >> d1.month_ >> d1.day_;
return in;
}
ostream& operator<< (ostream& out, const Date& d)
{
out << d.year_ << " " << d.month_<< " " << d.day_;
return out;
}
int main()
{
Date d1(2024, 5, 6);
cin >> d1;
cout << d1 << endl;
}
cout是ostream类的一个全局对象,cin是istream类的一个全局变量,<<和>>运算符的重载函数具有返回值就是为了实现连续的输入和输出操作。
友元函数说明:
- 友元函数可以访问类的私有和保护成员,但是不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以定义再类定义的任何地方声明,不受访问限定符的限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数调用原理相同
友元类
友元类中的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中非公有成员。
class A
{
friend class B;// B是A的友元类,B中成员函数可以直接访问A中非共有成员
public:
A(int a):
_a(a)
{}
private:
int _a;
};
class B
{
public:
void printA(A a)
{
cout << a._a << endl;
}
};
int main()
{
A a(1);
B b;
b.printA(a);
}
友元类说明:
- 友元关系是单向的,不具有交换性。类似上面的例子,B类是A类的友元类,B可以随意访问A的非公有成员,但是A仍然不能访问B的非公有成员
- 友元关系不能传递。若A是B的友元,B是C的友元,不能推出A是C的友元
内部类
概念:若一个类定义在另一个类的内部,则称这个类为内部类
说明:
- 此时内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象调用内部类
- 外部类对内部类没有任何特殊的访问权限
- 内部类是外部类的友元类,内部类可以通过外部类的对象参数访问外部类中所有成员。但是外部类不是内部类的友元
特性:
- 内部类可以定义在外部类的public,private,protected这三个区域任意地方
- 内部类可以直接访问外部类的static,枚举成员,不需要外部类的对象或类名
- 外部类的大小和内部类无关
再次理解封装
C++是面向对象的程序,面向对象的三大特性:封装,继承,多态
C++通过类,将一个对象的属性与行为结合在一起,使其更符合人们对于一件事物的认知,将属于该对象的所有东西打包在一起。通过访问限定符的将其部分功能开放出来与其他对象进行交互,而对于对象内部的一些实现细节,外部用户不需要知道,知道了有些情况下也没用,反而增加了使用或者维护的难度,让整个事情复杂化。