类和对象
C++支持面向对象编程,类是C++的核心特性,用于指定对象的形式和属性。类是C++最早引入的概念之一,是基于struct的数据集合类型,类里可以自定义成员和函数。用struct声明的结构体类型实际上也是类。用struct声明的类,默认其成员属性为public。
C++中,一个类有六个默认成员函数,分别是构造函数,析构函数,拷贝构造函数,=运算符重载函数,&运算符重载函数和const&运算符重载函数。
这些成员函数假若在定义时没有写,编译器会默认生成。
类访问限定符
数据封装是面向对象编程语言的一个特点,可以防止类外面的成员或函数直接对类内成员操作或访问等。有三个类访问限定符关键字:
class test
{
如果没有加限定符,权限默认为私有
public:
公有成员,类内外都可以访问与操作
private:
私有成员,类内、友元类可以访问,类外不可访问
protected:
被保护成员,类内、友元类可以访问,类外不可访问
与私有的区别:被保护成员在派生类中可以访问
};
this指针
C++中,每一个对象都可以通过隐含的this指针来访问自身。当我们调用成员函数,本质上是用这个对象调用公共代码段上的函数。this指针是一般成员函数的隐含参数,形式为class* const this,即this指针的指向不可以被修改,但是内容可以。如果想内容不被修改,可以这么写。
类的成员函数里,默认参数列表内第一个位置是this指针,如果在成员函数后面加const,可使this指针的值是不可以修改的。本质上是把this的形式从class* const this变为 const class* const this。
this指针本质上其实是一个成员函数的形参,存在于栈区,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针,更不存储成员函数的代码。并且拿指向空的对象的指针调用函数时会报错,对空指针取地址是错误的。
注意:友元函数、static修饰的静态成员函数没有隐含的this指针,因此不能用const来修饰。
成员函数
构造函数
构造函数在对象创建时自动调用,没有返回值,可以重载。函数名同类名。
class Box
{
public:
Box()
{
_length = 0;
_breadth = 0;
_height = 0;
}
private:
BoxT _length;
BoxT _breadth;
BoxT _height;
}
假设没有写构造函数,编译器生成的构造函数不会初始化内置类型(int等),因此长宽高会是随机值(可以在声明变量的时候给予缺省值,但是是在变量定义后才赋值,且先于其他构造函数的函数体内容)。
当类中有自定义类型时,生成的构造函数会自动调用该自定义类型的构造函数。因此构造函数最好每个类都自己写。
构造函数可以重载,有三种形式:全参数、无参数、全缺省。
目的就是让类即使不传参也能够默认初始化。
Box(BoxT length, BoxT breadth, BoxT height)
{
_length = length;
_breadth = breadth;
_height = height;
}
Box(const char* str, BoxT length = 10,
BoxT breadth = 20, BoxT height = 30)
{
}
初始化列表
构造函数还可以有初始化列表。初始化列表以冒号开头,后跟一系列以逗号分隔的初始化字段。
构造函数的执行可以分成初始化阶段和计算阶段,初始化阶段先于计算阶段。
初始化阶段:初始化列表在成员变量定义前执行,只生效一次,先于构造函数的函数体内容执行。
计算阶段:执行构造函数体内的赋值语句。
这就是初始化列表的书写形式,且初始化的顺序是根据变量声明的顺序而定,而不是列表的书写顺序,因此应尽量让变量声明的顺序和初始化列表书写顺序一致。
图中代码会先初始化str而不是length。
这有什么用?
让构造函数内部在干什么看起来更加直观?
一、当初始化内容有其他自定义类型。
这样写会有两次string的构造,因为实际是下面的情况。
首先是_str的string类的默认构造函数,其次是用字符串内容构造一个string然后拷贝赋值给str。这是性能方面的问题。
二、常量成员和引用类型
常量和引用有一个共同点,就是必须在定义时初始化,而不能先声明后赋值,且在初始化之后不能修改其值。这是语法方面的问题。
再注意:初始化的顺序是依照成员声明的顺序,而不是初始化列表写的顺序。因此写初始化列表的时候要按照声明顺序罗列成员,避免混淆。
结论:在定义时就需要初始化的变量,必须通过初始化列表初始化。
explicit关键字
单参数的构造函数支持隐式类型转换。explicit关键字只能用于单参数的构造函数, 表明该构造函数不支持隐式类型转换。
test(int size)
:_size(size), _data(new int[size])
{
for (int i = 0; i < _size; i++)
{
_data[i] = i;
}
}
test<int> t2 = 3;
这里可以拆解为:
test<int> tmp(3);
test<int> t2 = tmp;
涉及隐式类型转换,把整数3作为参数传入后构造一个test。
这个单参数构造函数加上explicit前缀会报错:
错误 C2440 “初始化”: 无法从“int”转换为“test<int>”
如果这是一个string类,则无法执行下列代码:
string str = "Hello";
但是有一种例外,当构造函数参数列表是半缺省时,explicit同样能起作用。
test(int size, int x = 10)
:_size(size), _data(new int[size])
{
for (int i = 0; i < _size; i++)
{
_data[i] = i;
}
}
test<int> t2 = 3;
“初始化”: 无法从“int”转换为“test<int>”
拷贝构造
拷贝构造函数由编译器调用来完成一些基于同一类的其他对象的构建及初始化。其形参必须是引用,一般加上const。此函数经常用在函数调用时用户定义类型的值传递。对于自定义类型,拷贝构造函数要调用子类的拷贝构造函数和成员函数。
如果没有自己写拷贝构造,编译器会默认生成。对内置类型会浅拷贝(值拷贝)。对于自定义类型,编译器会调用该类型的拷贝构造。
Box(const Box& box)
{
_length = box._length;
_breadth = box._breadth;
_height = box._height;
_volume = box._volume;
}
死递归问题
如果构造函数的函数参数不是传引用而是形参。传过来一个box,首先要构造一个这个box的临时拷贝作为形参,称为tmpbox,为了构造这个tmpbox,因为匹配到了拷贝构造的参数,会再调用拷贝构造函数,构造一个这个tmpbox的临时拷贝作为形参,称为tmptmpbox。。。循环往复导致死递归,因此传box的引用可以避免这个问题。
深浅拷贝
如果该类没有涉及内存分配,那可以不写拷贝构造,用编译器自己生成的,因为编译器默认生成的拷贝构造是浅拷贝,即值拷贝。
但是如果类中存在指针之类的变量,浅拷贝就会把另一个对象的指针的内容直接复制过来,导致原对象和另一个对象的指针指向了同一块空间,因此如果涉及增删查改等操作,就会操控同一块内存,造成误访问。
甚者,其中一个对象的生命周期结束后,析构会导致另一个对象的内容同样被析构,但是另一个对象的生命周期结束之后还是会调用一次析构函数,内存被delete两次程序就崩溃了!
所以应该手动申请一块新内存,再将原对象的指针指向新内存,然后依次拷贝另一对象的指针指向的值。
可以看到两个对象的指针都指向同一块内存。
手动写了拷贝构造后,两个对象的指针都独占一块内存,数据独立互不干扰。
运算符重载
运算符重载函数是特殊的函数,函数名是由关键名operator与要重载的符号构成的。
这里举例重载流插入和流提取运算符。
运算符重载的实现由自己决定,甚至可以用流插入运算符重载流提取操作。
比较好的例子:map[]重载既能直接插入K值,又能够返回V值的引用。
=运算符重载
自定义类的赋值运算符重载函数的作用与内置赋值运算符类似,但要注意,它与拷贝构造函数一样要注意深浅拷贝的问题。如果没有自己写赋值运算符重载函数,那么编译器同样会自动生成浅拷贝的赋值运算符重载函数。
=运算符重载函数不同于前面的构造函数,返回值是自身的引用,不支持初始化列表。
析构函数
析构函数也是一种特殊形式的函数,函数名同类型名,没有返回值,没有参数,不能重载。负责对象的销毁回收工作,在对象生命周期结束时自动调用。
~Box()
{
if(ptr)
delete[] ptr;
ptr = nullptr;
_length = 0;
_breadth = 0;
_height = 0;
_volume = 0;
cout << "析构" << endl;
}
析构函数在于回收资源,如果有动态申请内存,在结束时应释放内存,权限交回操作系统,避免内存泄漏。
同样的,如果不写析构函数,编译器也会自动生成,且自动生成的同样不会操作内置类型,对于自定义类型会去调用它的析构函数。
且构造和析构的顺序类似于栈的顺序。先构造的后析构。因为定义对象要调用构造函数,要建立函数栈帧,因此需要遵循先构造后析构的规则。
友元
友元提供了不同类的成员函数之间、类的成员函数与一般函数之间数据交互的机制。通过友元,一个不同函数或另一个类中的成员函数可以访问类中的私有和保护成员。
友元为封装的特性留了一扇窗,外界可以透过这扇窗窥探类的内部。
友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。
友元可以是一个函数,也可以是一个类。友元函数不是类成员函数。
友元函数
友元函数类内声明,类外定义,且可以访问对应类的私有和保护成员。
友元函数的声明可以放在类的私有部分,也可以放在公有部分
类内声明
private:
friend void printBox(const Box& box);
类外定义
void printBox(const Box& box)
{
}
友元类
比如B要访问A的私有成员,就在A里面声明B为A的友元类。
但是关系是单向的,且不可传递:B是A的友元,因此B可以访问A,但是A不能反过来访问B。如果B是A的友元且C是B的友元,C不会是A的友元。
且友元函数没有隐含的this指针,因为是在类外面实现的。
因此如果有需求,比如写运算符重载的函数,如果左操作数需要抢占this指针的位置,可以在类外定义,在类内声明为友元。
比如之前的流提取和流插入运算符重载。
friend std::ostream& operator<<(std::ostream& out, const Box& _box)
{
out << _box._length << '-' << _box._breadth << '-' << _box._height << '-' << _box._volume << endl;
return out;
}
friend std::istream& operator>>(std::istream& in, Box& _box)
{
in >> _box._length >> _box._breadth >> _box._height;
_box._volume = _box._length * _box._breadth * _box._height;
return in;
}
因为用法是:cout << box; 左操作数是ostream类型对象cout,右操作数是box类型对象box。
static关键字
static变量
静态成员是所有对象共享的,存放在静态区。
静态成员在类内声明后必须在类外定义,在类外定义时不受限定符的限制,但是之后的访问仍受限制。
当静态成员是公有的,有以下访问形式:
当静态成员是保护或者私有的,就不能直接访问,要通过函数接口间接访问。
static函数
static成员函数不能访问类内的非静态成员,因为没有this指针,但是可以访问静态成员,因为静态成员是属于类的,相当于通过类访问。