欢迎来到本期频道!
类和对象
- 类
- 定义:
- 格式:
- 类域:
- 访问限定符
- 友元
- 内部类
- this指针
- 静态与非静态成员关系
- 类型转换
- 六大默认成员函数(C++98)
- 1️⃣构造函数
- 2️⃣拷贝构造函数
- 浅拷贝与深拷贝
- 3️⃣赋值重载拷贝函数
- 4️⃣析构函数
- 5️⃣取地址重载
- 6️⃣const取地址重载
- 对象
- 类的实例过程
- 对象的大小
- 匿名对象
- 初始化列表
类
定义:
C++程序设计允许程序员使用类(class)定义特定程序中的数据类型。
格式:
class 类名 {} ;
分号不可省略!
花括号{}中的内容称为类的成员;
类中的变量称为类的属性或成员变量;
类中的函数称为类的方法或成员函数;
类的成员变量有非静态成员变量和静态成员变量(static修饰);
类的成员函数有非静态成员函数和静态成员函数(static修饰);
例如:
class Time
{
void print(int hour,int minute) //成员函数 方法
{
cout<<hour<<":"<<minute;
}
int _hour; //成员变量 属性
int _minute;
}
定义在类中的方法默认是inline函数 |
类域:
定义一个类,不仅仅只是定义了一个自定义类型,并且定义了一个新的作用域,既类域。
类的所有成员都在类的作用域中,既{}中;
如果在类外定义类成员时,需要使用作用域操作符::指定成员类域。
类域影响的是编译的查找规则 |
访问限定符
C++中一种实现封装的方式,用类将对象的属性和方法结合在一起,让对象更加完善,通过访问权限选择性的将其接口提供个外部的用户使用。
- public
- 该限定符修饰的成员在类外可直接访问
- private
- 该限定符修饰的成员在类外不能直接访问
- protected
- 该限定符修饰的成员在类外不能直接访问
创建类:
class Time
{
public:
void Init(int hour,int minute) //公有成员
{
_hour = hour;
_minute = minute;
}
void print() //公有成员
{
cout<<_hour<<":"<<_minute;
}
private:
int _hour; //私有成员
int _minute; //私有成员
};
小知识
:
struct也是定义类的关键字,只不过通常用class关键字,如果不写访问限定符,struct默认是public,class默认是private |
这里先简单了解一下,类类型在物理内存中创建对象的过程,叫做类实例化出对象。
#include<iostream>
using namespace std;
class Time
{
public:
void Init(int hour,int minute) //公有成员
{
_hour = hour;
_minute = minute;
}
void print() //公有成员
{
cout<<_hour<<":"<<_minute;
}
private:
int _hour; //私有成员
int _minute; //私有成员
};
int main()
{
Time t1; //Time类实例化出t1对象
int hour = 8;
int minute = 30;
t1.Init(hour,minute); //调用公有成员Init()
t1.print(); //调用公有成员print()
return 0;
}
这里不能使用t1._hour是因为访问限定符private限制不能在类外直接访问。
#include<iostream>
using namespace std;
class Time
{
public:
void Init(int hour,int minute) //公有成员
{
_hour = hour;
_minute = minute;
}
void print() //公有成员
{
cout<<_hour<<":"<<_minute;
}
private:
int _hour; //私有成员
int _minute; //私有成员
};
int main()
{
int hour1 = 8;
int hour2 = 9;
int minute1 = 30;
int minute2 = 40;
Time t1; //Time类实例化出t1
Time t2; //Time类实例化出t2
t1.Init(hour1,minute1);
t2.Init(hour2,minute2);
t1.print();
t2.print();
return 0;
}
友元
友元关键字friend,友元是一种突破类访问限定符封装的方式。
友元分为友元函数和友元类
性质:
- 外部友元函数可以访问类的所有成员
- 友元函数在类中仅仅是声明,不是类的成员函数
- 友元函数可以在类定义的任何地方声明,不受类访问限定符的限制
- 一个函数可以是多个类的友元函数
- 友元类的成员函数都是友元函数
- 友元类关系是单向的,没有交换性,没有传递性
class A
{
friend class B; //友元类
public:
void print_a()
{
cout<<_a<<endl;
}
private:
int _a;
};
class B
{
public:
void print_b() //是A的友元函数
{
A obj;
obj.print_a(); //访问公有成员
cout<<obj._a<<endl; //访问私有成员
}
};
虽然提供了便利,但是友元会增加耦合度,破坏了封装,不宜多用。 |
内部类
如果一个类定义在另一个类的内部,这个类放在内部的类叫做内部类。
性质:
- 内部类是一个独立的类,与定义在全局的类相比,它只是受到类域和访问限定符的限制
- 内部类默认是外部类的友元类
- 内部类本质上是一种封装,当A类与B类紧密关联时,就可以考虑内部类
this指针
当t1调用Init和print时,成员函数是怎么知道是t1在调用,还是t2在调用呢? |
其实编译器编译之后,都会在类的非静态成员函数的第一个形参位置添加一个当前类类型的指针,其真实原型为:
void Init(Time* const this, int hour, int minute);
void print(Time* const this, int hour, int minute);
类的非静态成员函数中访问成员变量,实质是通过this指针访问的。 |
那么能不能显示的传送指针或接收指针呢? |
c++规定不能在实参和形参位置显示的写this指针(编译器编译时处理),但是可以在函数体内显示使用this指针。 |
class Time
{
public:
void Init(int hour,int minute) //公有成员
{
this->_hour = hour; //只有类的非静态成员函数的函数体中可以显示写this
this->_minute = minute;
}
void print() //公有成员
{
cout<<this->_hour<<":"<<_minute; //写或者不写都可以,效果一样
}
private:
int _hour; //私有成员
int _minute; //私有成员
};
静态与非静态成员关系
在类中除了普通成员变量和普通成员函数
还有const成员变量,const成员函数
以及static成员变量,static成员函数
让我们观察一下静态与非静态的关系
示例如下:
class wealth
{
public:
void print() //普通成员函数 wealth* const this
{
//可以修改所有非const成员变量
_val = -1;
_sval = -2;
//可以访问所有成员变量
cout << _val << endl;
cout << _cval << endl; // _cval本身有const属性不能直接修改
cout << _sval << endl;
//可以调用所有成员函数(因为普通类型的成员函数也可以调用)
print_const();
print_static();
}
void print_const()const //const成员函数 const wealth* const this
{
//只能修改静态成员变量,因为_sval是通过类域指定的,而不是this指针
_sval = -3;
//可以访问所有成员变量
cout << _val << endl;
cout << _cval << endl;
cout << _sval << endl;
//只能调用static成员函数和自身const类型的成员函数
print_static();
}
static void print_static() //static成员函数 没有this指针
{
//没有this指针,无法访问或修改非静态成员变量以及非静态成员函数
cout << _sval << endl;
//只能访问或修改静态成员变量
//只能调用静态成员函数
}
private:
//声明+缺省值
int _val = 0; //普通成员变量
const int _cval = 1; //const成员变量
static int _sval; //静态成员变量
};
int wealth::_sval = 2;
总结:
类型转换
- c++支持内置隐式类型转换为类类型对象,需要相关内置类型为参数的构造函数
class A
{
public:
A(int x = 1,int y = 1)
{
_a = x;
}
private:
int _a;
int _b;
};
int main()
{
A obj;
obj = 3;
return 0;
}
- 类类型的对象之间也可以隐式转换,需要相应的构造函数支持
class A
{
public:
A(int x = 1)
{
_a = x;
}
int get_a()const
{
return _a;
}
private:
int _a;
};
class B
{
public:
B(const A& it,int c=1)
{
_b = it.get_a();
_c = c;
}
private:
int _b;
int _c;
};
- 构造函数前加explicit关键字就不再支持隐式类型转换
六大默认成员函数(C++98)
如果小伙伴看到这里,可能觉得类好像知道的差不多了,但是为了更好的封装对象,在c++98中,类还有六大默认成员函数。
- 构造函数
- 拷贝构造函数
- 赋值重载拷贝函数
- 析构函数
- 取地址重载
- const取地址重载
默认成员函数:用户没有显示实现,编译器自动生成的函数
以Time类为例:
class Time
{
public:
Time(); //构造函数
Time(const Time& t); //拷贝构造函数
Time& operator=(const Time& t); //赋值重载拷贝函数
~Time(); //析构函数
Time* operator&(); //取地址重载
const Time* operator&()const; //const取地址重载
private:
int _hour;
int _minute;
};
小知识
构造函数和析构函数不能加const |
1️⃣构造函数
定义:
与普通函数相比,构造函数不写返回值,函数名是该类类型名称。
- 构造函数分为默认构造和带参构造
- 默认构造函数- - 有3种
- 无参构造函数
Time() { //内容 }
- 全缺省构造函数
Time(int hour = 1, int minute = 1) { //内容 }
- 编译器自动生成的构造函数(没有显示写构造函数时才生成)
- 无参构造函数
- 带参构造函数
Time(int hour,int miute=1) { //内容 } Time(int hour,int miute) { //内容 } Time(const Time& t) //拷贝构造 { //内容 } //.....等等
构造函数的功能是什么? |
首先先看看构造函数的特点:
- 函数名与类名相同
- 不写返回值
- 构造函数可以重载
- 对象实例化时,自动调用对应的构造函数
- 如果类中没有显示定义构造函数,C++编译器自动生成一个无参的默认构造函数。(如果显示定义了,就不生成了)
- 默认构造函数只能存在一个或者没有,因为无参构造和全缺省构造虽然构成函数重载,但是存在调用歧义,所以二者只能留一个,既然显示定义了构造函数,那么就没有编译器生成的默认构造函数了;
如果没有定义任何构造函数,编译器才会生成默认构造函数。 - 编译器生成的默认构造函数对该类中的内置类型成员是否初始化取决于编译器,对类中的自定义类型,调用它的默认构造初始化。
我们可以从中得知构造函数是对象实例化时自动调用的特殊函数,通常用于初始化该类对象。 |
像Time类中内置类型没有指向资源的,可以按需要定义默认构造函数或者带参构造函数,可不写构造函数。
Time(int hour,int minute)
{
_hour = hour;
_minute = minute;
}
像stack类中内置类型指向资源的,最好建议使用默认构造函数,方便复用类型。
stack(int n = 4)
{
_arr = new int[n]; //申请资源
_size = 0;
_capacity = n;
}
2️⃣拷贝构造函数
定义:
拷贝构造是一个特殊的构造函数.
要求第一个形参是自身类类型的引用,其余形参必须要有缺省值
Time(Time& t)
{
//内容
}
Time(const Time& t)
{
//内容
}
Time(const Time& t,int x = 1,int y = 2)
{
//内容
}
//等等
拷贝构造是用一个已存在的对象去初始化一个新的对象
首先咱们看看拷贝构造函数的特点
- 拷贝构造函数是一个特殊的构造函数(构造函数的重载)
- 拷贝构造函数的第一个参数必须是当前类类型引用,如果使用传值方式,编译器直接报错(语法逻辑上引起无穷递归),其余参数必须有缺省值。
- C++规定自定义类型对象进行拷贝时(传值传参、传值返回),必须调用其拷贝构造函数。
- 如果没有显示定义拷贝构造函数,C++编译器会自动生成拷贝构造函数。
- 编译器自动生成的拷贝构造函数,对类中内置类型是值拷贝(浅拷贝),对类中自定义类型是调用其拷贝构造。
浅拷贝与深拷贝
为什么会有深浅拷贝? |
我们以栈为例:
class my_stack
{
public:
my_stack(int n = 4)
{
_arr = new int[n];
_size = 0;
_capacity = n;
}
void push(int val)
{
if(_size==_capacity)
{
//...扩容
}
_arr[_size++] = val;
}
//......方便示例,简写
private:
int* _arr;
size_t _size;
size_t _capacity;
};
int main()
{
my_stack obj1(10); //实例化出obj1
for(size_t i = 0;i < 10; i++) //在obj1中存储数据
{
obj1.push(i+1);
}
my_stack obj2(10);
obj2 = obj1; //将obj1拷贝给obj2
return 0;
}
将obj1拷贝给obj2,我们的目的是让obj2拥有obj1一样的数据
第一种方式,就是浅拷贝:
obj2._arr = obj1._arr;
obj2._size = obj1._size;
obj2._capacity = obj1._capacity;
接下来,我们看看深拷贝:
obj2._arr = new int[obj1._capacity];
for(size_t i = 0; i < obj1._size; i++)
{
obj2._arr[i] = obj1._arr[i];
}
obj2._size = obj1._size;
obj2._capacity = obj1._capacity;
总而言之,言而总之,有资源申请的类(或者有(释放资源的)析构函数),一定要用深拷贝,否则问题多多。 |
像Time类中的成员变量都没有指向资源的情况下,使用浅拷贝就可以了。(或者不写,编译器的就够用)
Time(const Time& t)
{
_hour = t._hour;
_minute = t._hour;
}
像Stack类中的成员变量_arr指向了堆的资源,需要我们手动的进行深拷贝。
#include<iostream>
using namespace std;
class stack
{
public:
stack(int n = 4)
{
_arr = new int[n];
_size = 0;
_capacity = n;
}
//拷贝构造
stack(const stack& st)
{
_arr = new int[st._size]; //申请一份新的资源空间
for(size_t i = 0; i < st._size; i++) //将st的资源空间的数据依次用赋值拷贝。
{
_arr[i] = st._arr[i];
}
//以上就是深拷贝
_size = st._size; //浅拷贝
_capacity = st._capacity; //浅拷贝
}
//.....为了方便示例,简写
private:
int* _arr;
size_t _size;
size_t _capacity;
};
3️⃣赋值重载拷贝函数
定义:
C++支持使用operator关键字对运算符重载;该函数是对赋值运算符的重载。
赋值重载拷贝是用一个已存在的对象去拷贝一个已存在的对象。
让我们看看赋值重载拷贝函数的特点
- 类类型的赋值运算符拷贝重载函数,规定必须重载为成员函数。
- 返回值建议写成当前类类型的引用,可以减少拷贝且可以连续赋值。
- 没有显示定义时,编译器会自动生成一个赋值重载拷贝函数,完成该类的浅拷贝。
- 编译器自动生成的赋值重载拷贝函数,对类中的内置类型完成浅拷贝,对类中的自定义类型调用其赋值重载拷贝函数。
Time& operator=(const Time& t)
{
//内容
}
既然是拷贝函数,那么也是分为浅拷贝和深拷贝
例如像Time类的成员变量没有指向任何资源,浅拷贝就可以了。
Time& operator=(const Time& t) //形参和返回值用引用可以减少拷贝
{
_hour = t._hour;
_minute = t._minute;
return *this;
}
像Stack类中的成员变量_arr指向了堆的资源,需要我们手动的进行深拷贝。
stack& operator=(const stack& st)
{
delete[] _arr; //释放旧空间
_arr = new int[st._size]; //申请一份新的资源空间
for(size_t i = 0; i < st._size; i++) //将st的资源空间的数据依次用赋值拷贝。
{
_arr[i] = st._arr[i];
}
//以上就是深拷贝
_size = st._size; //浅拷贝
_capacity = st._capacity; //浅拷贝
return *this;
}
4️⃣析构函数
定义:
与普通函数相比,析构函数不写返回值,没有形参(所以不支持函数重载),函数名是 ~类型名称。
析构函数特点:
- 函数名是类名前加上字符~
- 不写返回值,不写形参(没有重载函数)
- 对象生命周期结束时,系统会自动调用析构函数
- 当没有显示定义析构函数时,编译器会自动生成析构函数
- 编译器自动生成的析构函数对类中的内置类型不做处理,对类中自定义类型成员会调用其析构函数
- 即使用户显示定义了析构函数,对于类中的自定义类型成员也会调用其析构函数
- 一个局部域的多个对象,C++规定先定义的后析构。
这里我们明白,无论写不写析构函数,对于类中的自定义类型都会调用其析构函数。
所以我们只要关心类中的内置类型是否指向资源即可。
~Time()
{
//内容
}
像Time类中的内置类型成员变量,没有指向任何资源的,可以不写析构函数。
~Time()
{
_hour = _minute = 0;
}
像stack类中的内置类型成员变量指向了资源的,必须手动写析构函数来释放资源。
~stack()
{
delete[] _arr; //释放资源
_arr = nullptr;
_size = _capacity = 0;
}
5️⃣取地址重载
定义:对取地址符号重载,用于获取普通对象地址。
Time* operator&()
{
return this;
}
6️⃣const取地址重载
定义:对取地址符号重载,用于获取const对象地址。
怎么获取const对象的地址呢?前面我们说由于类中的非静态成员函数在经过编译之后,会在非静态成员函数的第一个形参位置添加this指针;
既
const Time* operator&(Time* const this)const;
该函数形参后面的const其实修饰的是*this,,我们把这类函数叫做const成员函数,它想表达的是这样:
const Time* operator&(const Time* const this);
但是c++规定合法的const成员函数如下:
const Time* operator&()const //const修饰的是对象
{
return this;
}
对象
类的实例过程
前面我们稍微提到了类的实例化,那么实例化大致是什么样的一个过程呢? |
事实上,一个类对象会在编译期间开辟好空间并对类的静态成员变量初始化,接着走初始化列表对类的非静态成员变量进行初始化,然后进入构造函数的函数体完成相关指令,从而得到一个完整的对象。 |
对象的大小
对象是由类实例化而来,而类中有变量和函数。
事实上在编译器编译之后,代码会编译成指令,这里类中的函数地址,其实不需要存储,这些函数都在代码区,不存在于对象中。
那么变量呢?
其实静态成员变量存在静态区,是整个类的共享变量,所以静态成员变量也不属于某个对象。
而非静态成员变量存在栈中,编译时开好的空间,就是为非静态成员变量开的,那么非静态成员变量有好几个,在这个对象的空间中,是随便放的吗?显然不是,不仅是按照声明顺序存储的,而且还要按照内存对齐规则存储。
class my_type
{
public:
my_type(int val = 1, float fval = 1.0)
{
_val = val;
_fval = fval;
}
void print() //非静态
{
cout<<_val<<endl;
cout<<_fval<<endl;
cout<<_sval<<endl;
}
static void print_s() //静态
{
cout<<_sval<<endl;
}
private:
int _val;
float _fval; //非静态
static int _sval; //静态
};
int my_type::_sval = 1;
匿名对象
用类型(实参)定义出来的对象叫做匿名对象。
int main()
{
Time(8,30).print(); //匿名对象调用方法
return 0;
}
性质:
匿名对象的生命周期只在当前一行。 |
初始化列表
什么是初始化列表?不是说构造函数是用来初始化的吗? |
初始化列表是在构造函数参数列表后以一个冒号开始,接着用逗号分隔的数据成员列表,每个初始化列表成员后跟一个放在圆括号中的初始值或表达式。
初始化列表是用来初始化对象的。
class Time
{
public:
Time(int hour,int minute)
{
_hour = hour;
_minute = minute;
}
//...
private:
int _hour;
int _minute;
};
class luck
{
public:
luck(int val, int cval,int& ref, int hour, int minute)
:_val(val) //初始化列表
,_cval(cval)
,_ref(ref)
,_t(hour,minute)
{
}
//...
private:
int _val;
const int _cval; //const成员变量
int& _ref; //引用
Time _t; //无默认构造函数的自定义类型
static int _sval;
static const int _scval;
};
int luck::_sval = 1; //静态成员变量在类外初始化
const int luck::_scval = 1;
为什么静态成员变量不能出现在初始化列表中? |
前面提到静态成员变量不在某个对象中,而初始化列表是用来初始化某个对象的(也就是对象这快空间),所以它不走初始化列表。
由以上可知初始化列表在语法上,可认为是每个非静态成员变量定义初始化的地方。 |
而构造函数体内使用的是赋值。 |
像Time类的构造函数并没有写初始化列表,但事实上,在进入构造函数体之前,每个非静态成员变量都会按声明顺序依次经过初始化列表,只不过在没有写初始化列表的情况下,对内置类型是否初始化取决于编译器,对自定义类型调用其默认构造函数;但是const成员变量,引用,无默认构造的自定义类型必须出现在初始化列表中。
无论有没有显示写,每个构造函数都有初始化列表。 |
注意:
初始化列表是按照声明顺序初始化的。 |
希望该片文章对您有帮助,请点赞支持一下吧😘💕