类和对象
1. 类的定义
class是定义类的关键字,name为类名,{}里的内容为类的主体,注意类定义结束后的;
不能省略。
class name
{
//类体由成员变量和成员函数组成
}; //分号不能省略
类的成员变量又称类的属性,类的成员函数又称类的方法
1.1 定义类的两种方式
1.声明和定义全部放到类中。注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理
class student
{
void show()
{
cout<<name<<age<<sex<<endl;
}
public:
char* name;
char* sex;
int age;
};
2.类声明放在.h文件中,成员函数定义放在.cpp文件中。注意:成员函数名前需要加类名::
//声明放在头文件student.h中
class student
{
void show();
public:
char* name;
char* sex;
int age;
};
//定义放在类的实现文件student.cpp中
#include"student.h"
void student::show()
{
cout<<name<<age<<sex<<endl;
}
在工程实践中,一般更倾向于使用第二种
2. 类的访问限定符
c++有三种访问限定符:public,protected,private
访问限定符的说明:
1.public修饰的成员在类外可以直接被访问
2.protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
3.访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
4.如果后面没有访问限定符,作用域到}
结束。
5.class的默认访问权限为private, struct为public(因为struct要兼容C)
注意:访问限定符只在编译的时候有效,当数据映射到内存后,没有任何访问限定符上的区别
c++实现封装的方式:用类将对象和属性与方法结合到一起,让对象更加完善,通过访问权限选择性地将其接口提供给外部的用户使用
3. 类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用
::
作用域操作符指明成员属于哪个类域。
class student
{
void show();
public:
char* name;
char* sex;
int age;
};
void student::show()//这里需要指明show是属于student这个类域
{
cout<<name<<age<<sex<<endl;
}
4. 类的实例化
用类型创建对象的过程,称为类的实例化
类定义了对象的结构和行为,可以看作是对象的模板或蓝图。这个蓝图画出了类有哪些成员,但实际上蓝图上的内容并没有实体,也就是说定义一个类并没有分配实际的内存空间来存储它。
//没有实例化,编译器会报错
class student
{
void show();
public:
char* name;
char* sex;
int age;
};
int main()
{
student.age=15;//编译失败 : error c2059 语法错误"."
}
—个类可以实例化出多个对象,实例化出的对象才会占用实际的物理空间
5. 类对象模型
类对象存储的方式:
一个类中可能有成员变量和成员函数,每个实例化的类中的成员变量都单独占据内存空间,但如果每个类中的成员函数也单数占据内存空间,那么将会对空间造成极大的浪费。所以实际上实例化的类中的成员函数会存入一个公共的代码区,这样即使实例化了成百上千个相同的类,类中的成员函数也只占据一份空间,避免了内存空间的浪费。
#include <iostream>
using namespace std;
class student
{
void show()
{
double scores;
long long code;
}
public:
char* name;
char* sex;
int age;
};
int main()
{
student s;
cout << sizeof(s) << endl;
return 0;
}
如果成员函数内的变量与成员变量存在同一个空间,那么s应占36个字节,可实际s只占了24个字节,说明成员函数被单独存在了另一个内存空间里
类(或结构体)内存对齐规则:
1.第一个成员在与结构体偏移量为0的地址处。
2.其他成员变量要对齐到某个数字 (对齐数)的整数倍的地址处。
注意:对齐数=编译器默认的一个对齐数与该成员大小的较小值。
VS中默认的对齐数为8
3.结构体总大小为:最大对齐数 (所有变量类型最大者与默认对齐参数取最小 的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
ps:没有成员变量的类如果被实例化出来,它的大小为1字节。
6. 隐含的this指针
C++编译器给每个非静态的成员函数“増加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
class student
{
public:
void show()
{
cout<<name<<age<<sex<<endl;
}
private:
char* name;
char* sex;
int age;
};
6.1 this指针的特性
1.this指针的类型:类类型 const*,即成员函数中,不能给this指针赋值。
2.只能在“成员函数的内部使用
3.this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
4.this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传
递,不需要用户传递。
ps:
1.c++规定this必须放在第一个参数,且不能显示写出来,只能由编译器自动调用。但是在函数的内部可以显示使用。
2.this指针存在栈上,因为它是一个函数的形参。(但是有些编译器可能会存在寄存器上,比如vs)
7. const成员
在类类型中,将const修饰的的成员函数称之为const成员函数。const修饰实际上是隐含的this指针,表示在该成员函数中不能对类的任何成员进行修改。
class student
{
public:
student()
{
math = 100;
chinese = 100;
english = 100;
}
student(int _math, int _chinese, int _english)
{
math = _math;
chinese = _chinese;
english = _english;
}
void print()
{
cout << "math=" << math << endl;
cout << "chinese=" << chinese << endl;
cout << "english=" << english << endl;
}
private:
int math;
int chinese;
int english;
};
因为print函数在调用的时候实际上是
print(*this)
,但const修饰的s1不允许修改成员变量,引发了权限放大的冲突。
对print
函数进行一个小改动:
void print() const//由于this不能显示写出来,所以规定对this指针的const修饰写在最后面
{
cout << "math=" << math << endl;
cout << "chinese=" << chinese << endl;
cout << "english=" << english << endl;
}
此时print虽然变成了const成员函数,s1和s2调用print函数都没有报错,因为虽然权限放大会引发冲突,但权限缩小是被允许的。
对于是否要使用const修饰成员函数,要看函数本身是否对成员变量进行修改。如果函数只读,那么最好写上const来适用const对象和非const对象;如果函数又读又写,那么不能用const修饰成员变量。
8. 类的六个默认成员函数
8.1 构造函数
8.1.1 构造函数的格式
构造函数负责初始化对象。构造函数是默认成员函数,即使在类里没有写,编译器也会默认生成一个,所以类在实例化的时候都会调用构造函数。无参构造函数、全缺省构造函数、编译器默认生成的构造函数都是默认构造函数,默认构造函数只能存在一个。
编译器默认生成的构造函数对内置类型(int,double…)不做处理,但对于自定义类型(class,struct)会调用它们的默认构造。
特点:
1.函数名和类名相同
2.无返回值
3.对象实例化时编译器自动调用对应的构造函数
4.构造函数可以重载
class student
{
public:
student()//默认构造函数,如果不写编译器会自动生成一个
{
math=100;
chinese=100;
english=100;
}
student(int _math, int _chinese, int _english)//构造函数可以重载
{
math=_math;
chinese=_chinese;
english=_english;
}
void print()
{
cout<<"math="<<math<<endl;
cout<<"chinese="<<chinese<<endl;
cout<<"english="<<english<<endl;
}
private:
int math;
int chinese;
int english;
};
int main()
{
student s1;//注意:为了区分实例化和函数调用,c++规定在实例化的时候如果无参不能加括号()
student s2(50,40,30);
s1.print();
s2.print();
return 0;
}
注意,全缺省的构造函数和默认构造函数在实例化的时候有歧义,不能同时存在。
8.1.2 初始化列表
初始化列表是每个成员变量定义初始化的位置。在构造函数{}
的语句只能称其为赋初值(因为初始化只能初始化一次,而构造函数内部可以进行多次赋值),初始化列表只允许一个变量进行一次初始化,所以初始化列表才是完成初始化的工具,并且有些成员变量必须走初始化列表。
初始化列表的格式:以一个;
开始,接着是一个以,
分隔的数据成受列表,每个”成员变量”后面跟一个放在()
中的初始值或表达式。
class test
{
public:
test(int _t = 0)//如果下面的初始化列表不写test类,程序在走初始化列表的时候会进入这个默认构造函数
;t(_t)//但如果把全缺省去掉(变成非默认构造函数),且初始化列表不写test类,程序会报错
{}
private:
int t;
}
class student
{
public:
student(int _math, int _chinese, int _english,int& x)//使用初始化列表初始化
:math(_math),
chinese(_chinese),
english(_english),
n(1),//对const成员变量使用初始化列表
ref(x),//对引用成员变量使用初始化列表
tt(1)//对自定义类型使用初始化列表,如果不写会走它的默认构造,但如果没有默认构造会报错
{}
private:
int math;
int chinese;
int english;
const int n;
int& ref;
test tt;
};
必须走初始化列表的成员变量:
1.引用成员变量
2.const成员变量
3.自定义类型成员(且该类没有默认构造函数时)
——————————————————————————————
其他成员变量可以走初始化列表,也可以走函数内部
在某些情况下,初始化列表和构造函数体内可以配合使用:
class student
{
public:
student(int _math, int _chinese, int _english,int& x)
:math(_math),
chinese(_chinese),
english(_english),
p((int*)malloc(4))//初始化列表也可以使用函数
{
if(p==nullptr)
{
perror("malloc fail");
}
}
private:
int math;
int chinese;
int english;
int* p;
};
使用初始化列表给指针开空间,再使用函数体对指针进行检查
8.1.3构造函数的特点
1.成员变量的缺省值是给初始化列表的,所以初始化列表支持的语句都可以写(比如使用函数)。
2.单参数的构造函数支持隐式类型转换。(ps:不同类型之间的赋值都会产生临时变量)
class A { public: A(int _a = 0) :a(_a) {} private: int a = 0; }; int main() { A a1(1); A a2 = 2;// 本质是2构造了一个临时对象,再拷贝构造 return 0; }
隐式类型转换的好处:
class A { public: A(int _a = 0) :a(_a) {} private: int a = 0; }; class Stack { public: void push(const A& a) { // } }; int main() { Stack st; A a1(1); st.push(a1); st.push(2);//2被隐式类型转换了,这些写更简便 return 0; }
但如果我们不希望发生隐式类型转换怎么办?只需要在构造函数前加一句
explicit
就可以了:// explicit A(int _a = 0) :a(_a) {} //
3.c++11支持多参数的隐式类型转换(c++98不支持)
class A { public: A(int _a = 0, int _b=0) :a(_a), b(_b) {} private: int a = 0; int b= 0; }; int main() { A a1={1,2}; return 0; }
8.2 析构函数
析构函数与构造函数的功能相反,析构函数对对象的资源进行销毁。但析构函数并不会销毁对象本身,销毁对象是由编译器来完成的,而对象在被销毁时会自动调用析构函数,完成对象中资源清理的工作。
默认生成的析构函数和构造函数类似,对于内置类型不做处理,但对于自定义类型会调用它们的默认构造函数。
特点:
1.析构函数的名称是字符~
+ 类名。
2.无参数无返回类型。
3.一个类只能有一个析构函数,如果不写编译器会自动生成一个默认析构函数。(注意析构函数不能重载)
4.对象生命周期结束时,c++编译器自动调用析构函数。
class student
{
public:
student(const char* _name)
{
name = new char(strlen(_name + 1));
strcpy(name, _name);
}
~student()
{
cout << "调用 " << name << " 的析构函数" << endl;
}
private:
char* name;
};
int main()
{
student s1("张三");
student s2("李四");
return 0;
}
对象的声明周期结束时,自动调用析构函数。
构造函数存在的好处是在对象完成它的使命后不用手动删除对象,但默认生成的构造函数不会自主free掉对象的空间,所以在写构造函数的时候,一定要记得完成释放内存空间的功能。
8.3 拷贝构造
8.3.1 拷贝构造的定义
拷贝构造是一种特殊的构造函数,它可以轻松创建一个与已经被实例化的对象一模一样的对象。
默认生成的拷贝构造函数,对于内置类型不做处理,但对于自定义类型会调用它们的默认构造函数。
特点:
1.拷贝构造只有一个形参,且形参必须传引用否则会引发无限递归,且要加const修饰。
2.若未显示定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
3.编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了。
c++规定自定义的类型都会调用拷贝构造
class student
{
public:
student()
{
math = 100;
chinese = 100;
english = 100;
}
student(int _math, int _chinese, int _english)
{
math = _math;
chinese = _chinese;
english = _english;
}
student(student& s)//拷贝构造必须传引用,否则会引发无限递归
{
math = s.math;
chinese = s.chinese;
english = s.english;
}
void print()
{
cout << "math=" << math << endl;
cout << "chinese=" << chinese << endl;
cout << "english=" << english << endl;
}
private:
int math;
int chinese;
int english;
};
int main()
{
student s1(50, 40, 30);
student s2(s1);
cout << "s1:" << endl;
s1.print();
cout << endl<< "s2:" << endl;
s2.print();
return 0;
}
8.3.2 浅拷贝与深拷贝
对于上面的例子,即使用编译器默认生成的拷贝函数(浅拷贝)也能完成复制。但对于一些特殊情况,浅拷贝就无法胜任了:
typedef int DataType;
class Stack{
public:
Stack(size_t capacity = 10){
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array){
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data){
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack(){
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main(){
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
由于浅拷贝的原理是按内存存储按字节序完成拷贝,所以在上面的情况中,s2在拷贝s1的_array
时拷贝的是的是地址,也就是说s1和s2的_array
指向了同一个空间:
当程序结束时,先调用s2的析构函数释放了_array
指向的空间,由于s1和s2的_array
指向的是同一块内存空间,当s1调用析构函数时,原本已经被释放的空间又会被释放一次,但此时这块区域已经还给系统,所以程序报错。
因此,在成员变量是指针,或有文件的情况下,不能使用简单的浅拷贝,需要自己编写拷贝构造函数。
8.3.3 拷贝构造的使用场景
1.使用已存在的对象创建新的对象。
2.函数参数类型为类类型对象。
3.函数返回值为类类型对象。
8.4 操作符重载
8.4.1运算符重载
运算符重载可以增强代码的可读性,是一种有特殊函数名的函数。为什么要重载运算符呢?因为c中的>、<、==
等运算符只能用来比较整形或字符的ascii码之间的大小。但类中的成员变量可能很多或含义与普通的数字有巨大差异,如果程序员能自定义运算符的比较规则,那么代码就更清晰明了了。
格式:
1.函数名为关键字operator
后面接需要重载的运算符符号。
2.返回值类型operator操作符。
operator重载的规则:
1.不能创建新的操作符:如operator@
2.重载操作符必须有一个类类型参数
3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this指针5.
.* :: sizeof ?: .
以上五个运算符不能重载!(*
可以重载,.*
是另一个操作符,用来作为成员函数的指针。)
用日期的比较作例子:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//d1<d2 -> d1.operator<(d2)
bool operator<(const Date& d)//重载<运算符
{
if (_year < d._year)
{
return true;
}
else if (_year == d._year)
{
if (_month < d._month)
{
return true;
}
else if (_month == d._month)
{
if (_day < d._day)
{
return true;
}
}
}
return false;
}
bool operator==(const Date& d)
{
return _year == d._year && _month == d._month && _day== d._day;
}
bool operator<=(const Date& d)
{
return *this < d || *this == d;//可以通过使用已经重载好的运算符来简化其他运算符重载的代码
}
bool operator>(const Date& d)
{
return !(*this < d)
}
bool operator>=(const Date& d)
{
return !(*this < d) || *this == d;
}
bool operator>=(const Date& d)
{
return !(*this == d);
}
private:
int _year;
int _month;
int _day;
};
8.4.2 流插入和流提取运算符
c++为了让类类型也能使用流插入和流提取,提供了其重载的功能,但在重载它们时会有一点特殊。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void operator<<(ostream& out)
{
out<<_year<<"年"<<_month<<"月"<<_day<<"日"<<endl;
}
private:
int _year;
int _month;
int _day;
};
这里的代码“cout流入d1”是正确的,但“d2流入cout”是错误的,显然与代码的含义不符。这是因为成员函数重载的第一个参数必须是this指针,Date就变成左操作数了。
如果想要让ostream& out
作为第一个参数,成员函数就必须写在全局上变成全局函数。但全局函数不能访问private
的成员变量。
能不能即让ostream& out
作为第一个参数,又不让private
的成员变量变成public
呢?
这时友元声明就发挥了它的作用:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
friend ostream& operator<<(ostream& out, const Date & d);//友元函数拥有成员函数相同的权限
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date & d)//全局函数中可以让ostream& out作为第一个参数
{
out<<d._year<<"年"<<d._month<<"月"<<d._day<<"日"<<endl;
}
通过使用友元声明,可以保持成员变量的访问限定符不变,又让流提取的代码形式符合其含义
//流插入
istream operator>>(ostream& out, const Date & d)
{
cout>>"请输入年 月 日"<<endl;//可以在插入时在屏幕上输出一些提示信息
cin<<d._year<<d._month<<d._day<<endl;
}
8.4.3赋值运算符重载
c++为了让类类型1也能使用=
来实现赋值,引出了赋值运算符重载。
由于赋值重载也是6个默认成员函数之一,所以即使不写编译器也会自动生成一个。对于内置类型编译器进行浅拷贝,对于自定义类型会调用它们的默认赋值重载。
赋值运算符重载只能作为类的成员函数,不能重载成全局函数。因为类自动生成的默认赋值运算符重载会和全局的赋值运算符重载冲突。
赋值运算符重载方式:
1.参数类型:
const T&
传递引用可以提高传参效率。2.返回值类型:
T&
返回值引用可以提高返回的效率,有返回值目的是为了支持连续赋值3.**返回
*this
😗*要符合连续赋值的含义
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//d1 = d2
Date& operator=(const Date& d)//注意返回值如果不写&,返回的就是*this的拷贝,会造成资源浪费
{
_year=d._year;
_month=d._month;
_day=d._day;
return *this;
}
private:
int _year;
int _month;
int _day;
};
8.5 取地址重载(普通和const)
class student
{
public:
student* student&()
{
return this;
}
const student* student&() const
{
return this;
}
};
这两个函数一般不需要重载,使用编译器自动生成的就够用了。除非希望用户在使用的时候取到特定的地址(比如不打算让用户知道对象的真实地址)。