类和对象
- 类的定义
- this指针
- 类的6个默认成员函数
- 构造函数
- 析构函数
- 拷贝构造函数
- 赋值运算符重载
- 赋值运算符重载
- 运算符重载
- const成员
- 取地址操作符重载
- const取地址操作符重载
- 初始化列表
- explicit关键字
- static成员
- 匿名对象
- 友元
- 内部类
- 拷贝对象时编译器的优化
类的定义
c++类的定义形式为:
class className
{
pubilic:
//...
private:
//...
};
在类的内部可以定义变量和函数,c++可以通过3个访问限定符来限制类的成员的访问权限:
pubilc:在该作用域的成员在类外可以直接访问
private和protected:在该作用域内的成员在类外不能直接访问
c++为了兼容c,就将c的结构体提升成了类,使用struct和class定义类的唯一区别是:class的成员的默认访问权限为private,struct的成员的默认访问权限是public,其他的并无区别。
在类里面定义的成员函数,编译器会将其当成内联函数来处理,但是否展开,最终还是取决于编译器。我们建议将短小的函数直接在类里面定义,其他的函数则声明和定义分离。
对象是类类型的实例化,类类型是对实例化对象的描述,对于一个类实例化出的多个对象来说,除了成员变量用来存储不一样的数据,成员函数都是一样的,因此为了节省空间,对象只保存成员变量,而成员函数则放在公共的代码段。由此,一个类的大小只需计算成员变量的大小即可,其计算方法与计算结构体的大小的方法一致结构体大小的计算。对于空类,编译器为了标识该类的存在,给了其一个字节的大小。
this指针
c++编译器给每一个非静态成员函数增加了一个隐藏的指针参数,该指针指向当前对象,当对象调用成员函数时就可以通过该指针访问该对象的成员变量,该指针由编译器自行传递,不需要用户来完成,用户可以在类里面显示使用this指针。
//定义了一个类
class Student
{
pubilc:
void Print()//这里有一个默认this指针,相当于void Print(Student *this)
{
cout<<_name<<_age<<_sex;
//cout<<this->_age,显示使用this指针
}
private:
_name[20]="zhangsan";
_age=20;
_sex[7]="male"
};
int main()
{
Student s;
//调用打印函数打印学生信息
s.Print();//相当于s.Print(&s)
return 0;
}
this指针特性:
1.this指针的类型为:类类型*const(如Student *const this),因此不能给this指针赋值。
2.this指针只能在成员函数内部使用。
3.this指针本质上是成员函数的形参,所以对象中不存储this指针(同普通函数参数一样,存放在栈区,VS存放在寄存器)。
4.this指针是成员函数参数列表隐藏着的第一个参数。
5.this指针可以为空。
class Student
{
pubilc:
void Print()
{
cout<<“c++”;
}
private:
_name[20]="zhangsan";
_age=20;
_sex[7]="male"
};
int main()
{
Student* s=nullptr;
s->Print();
//由于成员函数不在对象中,此处不需要解引用,故代码可以正常执行
//此时this指针为空
return 0;
}
class Student
{
pubilc:
void Print()
{
cout<<_age;
}
private:
_name[20]="zhangsan";
_age=20;
_sex[7]="male"
};
int main()
{
Student*s=nullptr;
s->Print();
//该代码编译通过,但运行崩溃
return 0;
}
类的6个默认成员函数
默认成员函数是当用户没有显式定义时,编译器自动生成的函数。
构造函数
构造函数是名字与类名相同的函数,创建对象时由编译器自动调用,用于给数据成员一个初始值,即初始化对象,该函数在对象整个生命周期内只调用一次。同时构造函数无返回值,可以重载,在调用无参的构造函数时后面不需要跟括号。
class Student
{
pubilc:
//函数1
Student(int height,int weight)//用户显式定义构造函数
{
_height=height;
_weight=weight;
}
//函数2,无参
Student()//构成重载
{
;
}
private:
int _height=170;
int _weight=60;
};
int main()
{
Student s1(64,177);//创建时自动调用构造函数1
Student s2;//创建时自动调用构造函数2
//s2后面不需要跟括号,即不能写成 Student s2(); 否则就成了函数声明
}
当用户没有显式定义构造函数时,编译器会自动生成一个无参的默认构造函数,该默认构造函数对成员变量的处理方式为:对内置类型成员不做处理,对自定义成员调用其默认构造函数 (默认构造函数只有无参的构造函数、全缺省的构造函数、编译器自动生成的构造函数3种)。
class Date
{
public:
Date(int year)//用户显式定义了一个参数不是缺省值的构造函数
{
_year=year;
}
private:
_year;
};
class Student
{
pubilc:
//用户没有显示定义构造函数,由编译器生成默认构造函数
private:
int* p;//不处理
int _height=170;//不处理
int _weight=60;//不处理
Date d1;//调用Date类的默认构造函数
//由于Date类没有默认构造函数,故出错
};
默认构造函数只能有一个
class Date
{
public:
Date(int year=1)
{
cout << "有参数" << endl;
}
Date ()
{
cout << "无参数" << endl;
}
private:
int _year = 60;
int _month = 0;
int _day = 0;
};
class Student
{
public:
private:
int height;
int weigth;
Date d;
};
int main()
{
Date d1(1);//调用有参的构造函数
Date d2;//对重载的构造函数调用不明确,出错
Student s;//自定义类型调用其默认构造时不明确,出错
}
由上我们可以得知当成员变量都是内置类型时构造函数可以不写,但大多数情况下都要写构造函数。
析构函数
析构函数的功能与构造函数的功能相反,用于对成员变量的资源的清理,在对象销毁时会自动调用析构函数,析构函数不能重载,参数列表为空。
class Date
{
public:
Date()
{
_s=new int;
}
~Date()//析构函数
{
delete s;//进行资源清理
}
private:
int*_s;
}
当用户没有显式定义析构函数时,同构造函数一样,编译器会自动生成一个默认析构函数,该默认析构函数对成员变量的处理方式为:对内置类型成员不做处理,对自定义成员调用其析构函数。
当类里面没有资源申请时,析构函数可以不写,使用编译器生成的默认析构函数就可以了。
拷贝构造函数
在用已经存在的类对象创建新对象时会调用拷贝构造函数,如当参数传值为一个类对象、返回一个类对象时等。拷贝构造函数是构造函数的一个重载形式,参数只有一个且必须是类类型对象的引用。
class Date
{
public:
//参数一定要是引用,如果不是引用,使用拷贝构造函数要进行值拷贝,
//就会又去调用拷贝构造函数,从而引发无穷递归
Date(const Date& d)//拷贝构造函数,使用const使代码更健壮
{
_year=d._year;
_month=d._month;
_day=d._day;
}
private:
int _year = 60;
int _month = 0;
int _day = 0;
};
int main()
{
Date d1;
Date d2=d1;//调用拷贝构造函数
}
如果用户没有显式定义拷贝构造函数,默认的拷贝构造函数进行的是浅拷贝(值拷贝),对成员变量的处理方式为:对内置类型成员进行浅拷贝,对自定义成员调用其拷贝构造函数。
如果类里面没有涉及到资源的申请时,拷贝构造函数可以不写,但当涉及到资源申请时,拷贝构造函数一定要写,否则是浅拷贝,容易出错。
class Date
{
public:
Date()//构造函数
{
_s=new int;
}
~Date()//析构函数
{
delete s;//进行资源清理
}
private:
int*_s;
}
int main()
{
Date d1;
Date d2=d1;//不会再调用构造函数
//以上代码运行时出错
//Date类里面进行的是浅拷贝,d1和d2里面的_s指向同一块空间
//对象d1、d2销毁时都要调用其析构函数,对同一块空间释放了2次,出错
return 0;
}
赋值运算符重载
赋值运算符重载
当要对一个已经创建好的对象进行赋值操作时,需要调用赋值运算符重载函数
class Date
{
public:
Date& operator=(const Date& d)//赋值运算符重载
{
if(this!=&d)
{
_year=d._year;
_month=d._month;
_day=d._day;
}
return *this;
}
private:
_year;
_month;
_day;
}
int main()
{
Date d1;
Date d2;
d2=d1;//调用赋值运算符重载
}
该函数要注意以下4点:
1.为了符合连续赋值,函数需要返回*this,同时为了提高返回的效率,需要用到引用返回。
2.为了提升传参效率和增强代码健壮性,参数应为引用且使用const修饰。
3.要检测是否是自己给自己赋值。
原因可以参考这里l1dian11的博客
4.赋值运算符只能重载成类的成员函数,不能重载成全局函数。
当用户没有显式定义时,编译器会自动生成一个默认的运算符重载,以值的方式逐字节拷贝,对成员变量的处理方式为:对内置类型成员直接赋值,对自定义成员调用其对应的赋值运算符重载。
运算符重载
c++除了支持赋值运算符重载外,还支持其他的运算符重载,只不过编译器不会自动生成这些运算符重载,需要用户显式定义。
其有以下几点需要注意:
1.只能重载已有的运算符,不能通过其他符号重载新的运算符,如不能重载@
2.重载类型必须有一个类类型参数(防止用户改变该操作符原来的对内置类型的运算)
3.以下5个运算符不能重载:
.* :: sizeof ?: .
这里说一下比较特殊的运算符重载:
1.前置++和后置++重载
由于这两个运算符的重载无法直接区分,c++进行了特殊处理:在参数列表增加一个int型参数表示后置++
class Date
{
public:
Date operator++()//表示前置++运算符重载
{
//...
}
Date operator++(int)//表示后置++运算符重载
{
//...
}
private:
_year;
_month;
_day;
};
2.流插入<<和流提取>>的运算符重载
cout是ostream类的对象,cin是istream类的对象
class Date
{
public:
ostream& operator<<(ostream& out)
{
//...
}
private:
_year;
_month;
_day;
};
//用法如下
Date d;
d<<cout;
虽然重载成功了,因为this指针默认占了第一个参数,所以其使用方式很奇怪,不符合我们使用的习惯,因此我们只能将其重载成全局函数。
class Date
{
public:
//使用友元使类外的函数可以访问类里面的私有成员
friend ostream& operator<<(ostream& out,const Date& d);
private:
_year;
_month;
_day;
};
friend ostream& operator<<(ostream& out,const Date& d)
{
//...
}
const成员
大多数情况下我们并不希望成员函数拥有对类里面的成员进行修改的权限,因此我们希望对this指针使用const进行修饰。
class Date
{
public:
void fun() const
{
//...
}
以上函数相当于void fun(const Date* const this)
//第2个const是this指针自带的
private:
_year;
_month;
_day;
};
我们建议只要成员函数不涉及到对成员变量的修改,后面都要加上const进行修饰。
取地址操作符重载
用于对一个普通对象取地址
class Date
{
public:
Date* operator&()
{
return this;//一般是直接返回this即可
//如果写成return 0x11223344;
//那么用取地址符获取对象地址时将全都是0x11223344这个地址
}
private:
_year;
_month;
_day;
};
这个一般不需要重载,使用编译器默认生成的即可。
const取地址操作符重载
用于对const修饰的对象取地址
class Date
{
public:
const Date* operator&() const
{
return this;//一般是直接返回this即可
//如果写成return 0x11223344;
//那么用取地址符获取对象地址时将全都是0x11223344这个地址
}
private:
_year;
_month;
_day;
};
这个一般也不需要重载,使用编译器默认生成的即可。
初始化列表
类在实例化成对象时,所有的成员变量都会在初始化列表中进行定义并给予变量相对应的值,内置类型如果没有显式地写在初始化列表,则会在初始化列表中给予其一个随机值,对自定义类型,会去调用其默认构造函数。初始化列表和构造函数可以混合使用。
c++11打了补丁,允许其在声明时赋值,这些值其实都是缺省值,用于给初始化列表。
class Date
{
public:
Date(int year,int month,int day)
:_year(2)
,_month(2)
{
_day=2;
}
private:
_year=1;
_month=1;
_day=1;
};
int main()
{
Date d(3,3,3);
//d._year=2,d._month=2,d._day=2;
}
类里面成员变量在类中的声明次序就是初始化列表的初始化顺序,与其在初始化列表中的顺序无关。
explicit关键字
如果类的构造函数只有一个参数或者除第一个参数无默认值其余均有默认值,则该类可以支持隐式转换。
class Date
{
public:
Date(int year,int month=1,int day=1)
{
_year=year;
_month=month;
_day=day;
}
private:
_year;
_month;
_day;
};
int main()
{
Date d=2019;//_year=2019,_month=1,_day=1
//将2019转换成Date(2019,1,1),再赋给d
}
c++11还支持多参数的隐式类型转换
class Date
{
public:
Date(int year,int month,int day=1)
{
_year=year;
_month=month;
_day=day;
}
private:
_year;
_month;
_day;
};
int main()
{
Date d={2019,10,11};//_year=2019,_month=10,_day=11
}
有时候我们并不希望这种隐式类型转换的发生,只需在构造函数前面加上explicit关键字即可,但这个关键字不能阻止强制类型转换的发生。
static成员
在类里面以static关键字修饰的成员称为类的静态成员,对于静态成员变量,其只能在类里面进行声明,不能给缺省值。
静态成员有以下特性:
1.静态成员为所有类对象所共享,不属于某个对象,存放在静态区。
2.静态成员变量在类内只是声明,必须要在类外定义,定义时不需加static关键字。
3.静态成员可以直接通过类名::静态成员或者对象.静态成员来访问
4.静态成员没有this指针,不能访问任何非静态成员。
6.静态成员也是类的成员,受public、private、protect访问限定符的限制。
class Date
{
public:
explicit Date(int year=1,int month=1,int day=1)
{
++i;
_year = year;
_month = month;
_day = day;
}
static int i;
private:
int _year;
int _month;
int _day;
};
int Date::i = 0;
int main()
{
Date d1;
Date d2;
Date d3;
Date d4;
Date d5;
cout << Date::i << endl;//i=5;
return 0;
}
匿名对象
c++允许匿名对象,可以拥有充当临时变量的作用,其生命周期只在这一行,该行执行完就销毁。
class Date
{
public:
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1 = Date(1, 2, 3);//匿名对象
Date d2 = {1,2,3};//隐式转换
return 0;
}
友元
友元分为友元函数和友元类
友元函数是定义在类外部的普通函数,不属于任何类,在类里面声明,可以直接访问类的私有成员,其有以下特性:
1.友元函数不能用const修饰
2.友元函数可以定义在类定义的任何地方声明,不受访问限定符的限制
3.一个函数可以是多个类的友元函数
4.友元函数与普通函数的调用原理相同
友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类里面的私有成员。
友元关系是单向的,没有传递性,不能继承。
虽然友元为访问私有成员提供了便利,但破坏了封装,增加了耦合度,不建议过多使用。
内部类
内部类是指定义在另一个类内部的类,他是一个独立的类,不属于外部类,也不能通过外部类的对象访问内部类的成员,可以认为外部类对内部类没有任何优越的访问权限。但内部类却是外部类的友元类,即内部类可以通过类外部的对象参数访问外部类的所有成员。
需要注意外部类的大小和内部类没有任何关系。
拷贝对象时编译器的优化
大部分编译器会对连续的构造或拷贝构造进行优化,如
连续的构造+构造优化为一个构造
连续的构造+拷贝构造优化为一个构造
连续的拷贝构造+拷贝构造优化为一个拷贝构造
不同的编译器的优化方式和程度不同。