C++_类和对象
一.封装性(访问限定符实现)
-
数据和方法封装到一起
-
访问限定符
三种访问限定符:public,protected,private
1.public成员可从类外直接访问,private/protected成员不能从类外直接访问
2.访问限定符的作用域是从出现到下一个限定符出现之前,或者到类体的结束前
3.类体中如果没有访问限定符,默认private
4.类的访问限定符体现了面向对象的封装性
5.class的默认权限是private,struct的默认权限是public
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
与结构体的不同:有访问限定符,结构体默认是共有 如果不想用访问限定符限制,C++就用struct
耦合度:独立性
作用域:局部域,全局域,类域,名字空间域(花括号括起来的都可以看做一个域)
类的作用域:
1.每个类都有自己的作用域,类的成员都在这个类的作用域中,成员函数内可任意访问成员变量和其他的成员函数
2.对象可以通过.访问公有成员,对象的指针通过->来访问公有成员
3.头文件放声明,不用定义类的方法,声明不开辟空间,定义分配空间
class Person 在类外定义
{ void Person::fun()
void fun(); {;}
}; .h中
在使用一个变量前,必须先进行声明,在类中,成员变量在函数之后,为什么编译器不报错?
在类中定义的名称(类的成员变量和成员函数)的作用域为整个类,作用域为整个类的名称只在该类中是已知的
类实例化对象 Person p;
1.一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间存储类成员变量 (类中的函数放在代码段)
2.类只是模型一样的东西,限定有哪些成员,定义一个类并没有分配实际的内存空间
3.类和对象就像图纸和房子
4.对象的大小为类中的所有成员变量的大小之和,同时也要遵循内存对齐
内存对齐
1.第一个成员从结构体偏移量为0的地址处开始存放
2.其他成员变量要对齐到某个数字(1对齐数)的整数倍的地址
//对齐数=编译器默认对齐数 与该成员大小的较小值
VS中默认为8 gcc中的默认值为4
3.结构体总大小为最大对齐数(每个成员变量除了第一个成员都有一个对齐数)的整数倍
4.如果嵌套结构体,嵌套结构体对齐到自己最大对齐数的整数倍处,整体大小仍要满足最大对齐数(包含嵌套结构体的对齐数)的整数倍
为什么要内存对齐?(面试题)
内存的访问不是任意位置的访问 CPU默认一次访问4个字节或8个,按内存对齐可以更快访问到需要的内容
是一种以空间换时间的机制
空类的大小是什么?为什么?空类就什么都没有吗?(面试题)
一个字节,是为了占个位置,表示这个类型的对象存在过,有六个默认成员函数
封装性体现在两个方面:类封装了成员和方法,访问限定符
面试题:如何在类外访问一个类中的成员变量
1.设置set/get接口
2.友元函数//友元类
this指针
1. this指针的类型:类类型 * const
-
this指针并不是对象本身的一部分,不影响sizeof的结果
-
this指针的作用域在类的非静态成员函数内部
-
this指针是类成员函数第一个默认的隐含参数,编译器自动维护,不用显式传递
-
只能在类的非静态成员函数中使用,其他函数不可以
this指针遵从_thiscall调用约定:
1._thiscall只能用在成员函数上
2.参数从右向左压栈
3.参数个数确定,this指针通过ecx传递给被调用者;如果参数不确定(_cdecl),this指针在所有参数被压栈之后再压入堆栈
4.对参数个数不确定的,调用者自行清理堆栈,否则函数自己清理
this指针可以为空,但不要调用有this指针的地方,会访问非法内存 Date * d=NULL; 此时this的值为NULL
为什么使用的是指针,而不是引用。
this指针的类型和类引用的类型都是Test* const但是,由于历史原因,C++最初是没有引用,而有指针的,在设计类时,就采用了指针的方式,就导致了现在的局面。虽然引用理论上也可以实现this的功能。
(5)this指针不能在构造函数的初始化列表中给对象的成员变量赋值,
初始化列表本义是在创建类的实例对象时,给其中成员变量赋初值。即此时对象还未创建完毕。而this指针是类在创建之后,由编译器自动生成的指针,指向对象的首地址。简单来说,先有对象,后有this指针。所以this指针不能在初始化列表中给成员指针赋初值。
this指针的调用原理就是指向类的起始位置(取类的地址),通过加上偏移量找到对应的成员变量,再对相应的成员变量进行操作
日期类-->时间网站
成员变量加_和参数区分
this(就是一个形参)-->存在栈上 VS中通过ecx传递
定义和初始化分开就不是原子操作
类的指针可以为空,且能正确调用没有this指针的成员函数
类的6个默认成员函数:构造函数,拷贝构造函数,析构函数,赋值运算符重载,取地址操作符重载,const修饰的取地址操作符重载
构造函数的作用:
1.构造&初始化对对象(原子操作)
2.类型转换,对于单参的构造函数,可以将其接收参数转化成类类型对象
用explicit修饰构造函数,抑制由构造函数定义的隐式转换,explicit关键字类内部的构建声明上,在类外的定义上不用再写
class Date
{
public:
Date(int n)
{
_year = n;
cout << _year << " " << _month << " " << endl;
}
Date()
{
cout << _year << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
d = 19.2; 此时d先调用 Date(),然后再调用Date(int n),发生了强制类型转换,若前面加了explicit关键字,编译器会报错,找不到可用函数,d的_year也变为19
过程:使用构造函数Date(int n)来创建一个临时的Date对象,并将19.2作为初值。随后,执行函数中的以下语句,将该临时对象中的内容复制到d中
隐式类型转换只发生在只接收一个参数的构造函数,这个过程叫隐式转换,自动进行
构造函数为什么不能为虚函数?
构造函数为什么不能用const修饰?构造函数的作用是构造和初始化对象,加了const无法对成员变量值进行修改,也就无法初始化了
构造函数的特性:函数和类名相同,没有返回值,编译器自动调用,可以重载(拷贝构造函数),缺省的构造函数(无参和带缺省值)只能有一个,有初始化列表,没有显示定义,编译器自动合成
在拷贝构造函数中,为什么对象可以直接访问私有的成员变量,也可以用初始化列表
1.在类的成员函数中可以直接访问同类的私有/保护成员
2.C++的访问限定符是以类为单位的,也就是说这个单位内的成员可以互相访问
拷贝构造函数必须使用类类型的引用传递参数(传值会引发无穷递归,没有出口,编译报错)
如果没有显示定义,编译器自动默认合成,会对每个成员变量依次拷贝(若拷贝malloc等申请空间的函数,会释放两次,因为直接拷贝了地址)
Date d(2018,4);
Date d1= d; 也会自动调用拷贝构造函数
不能重载的运算符(面试题)
5个不能重载的运算符:•(成员选择符)/.*(成员对象选择符)/::(域解析操作符)/?:(三目运算符)
除了赋值号(=)外,基类中被重载的操作符都将被派生类继承
前置++ ++d-->d.operator++(&d) Date operator++()
后置++ d++-->d.operator++(&d,0) Date operator++(int)
在重载运算符=时,要加判断if(this != &d)避免多余的赋值
//两个日期比较大小
bool operator < (const Date& x1,const Date& x2)
{
if (x1._year<x2._year)
return true;
else if (x1._year == x2._year)
{
if (x1._month < x2._month)
return true;
else if (x1._month == x2._month)
{
if (x1._day < x2._day)
return true;
else
return false;
}
}
}
cout << operator<(d1, d2) << endl; 调用
d1<d2->d1.operator<(&d1,d2)
深度探索构造函数
初始化列表不写,编译器也会自动初始化---自定义类型的成员变量
初始化列表:尽量使用初始化列表初始化,因为更高效(两个类嵌套,一个类在另一个类的构造函数中初始化)
初始化列表可以认为成员变量定义的地方
仅用于初始化类中的成员变量
一定要在初始化列表初始化的数据
尽量避免使用成员初始化成员(会调用其他函数),初始化顺序和定义顺序一致
const类型数据,引用,没有缺省构造函数的自定义类型的成员变量
哪些类必须自己写构造函数:有const类型的成员变量,引用
成员变量按声明顺序依次初始化,而非初始化列表出现的顺序
Date(int year, int month, int day,int time ,int minute,int second)
:_year(year)
, _month(month)
, _day(day)
{
_year = year;
_month = month;
_day = day;
_t = Time (time, minute, second); //此处使用匿名对象,因为用一次就不再用了
}
const修饰成员函数,实际修饰的是隐含的this指针,表示函数中不能对类的成员进行修改
const修饰成员变量,一定要在初始化列表初始化
在成员函数
const Date* 修饰指向的对象
Date const* 修饰指向的对象 const在*之前修饰的是指向的内容
Date * const *在const之后修饰的是指针变量本身
const Date* const 两者都指向
const不能传给非const
在const修饰的成员函数中要对类的某个数据成员进行修改,该数据成员定义或声明时必须加mutable关键字
class Date
{
public:
void Set() const
{
this->_month = 10;
}
private:
int _year=2018;
mutable int _month; //可以在const的情况下对变量进行修改
};
const Date d;可以调用以下函数
void Display()const
取地址运算符(这两个默认的成员函数一般不用重新定义)(可以让别人取不到你的地址)
Date* operator& ()
{
return this;
}
const Date* operator&() const
{
return this;
}
析构函数
特性:一个类中有且只有一个,若未显式定义,编译器自动生成 无参数无返回值,~类名,对象生命周期结束时,编译器自动调用,析构函数的作用不是删除类的实例化对象,而是做一些清理工作
内联(inline)
减少函数压栈的开销,提高程序的运行效率
在realease调试下,是一种以空间换时间的做法,代码很长或者有递归的函数不适合使用内联
内联就是建议性函数(代码很长或者递归循环不适合使用内联函数 ,内联对于编译器只是个建议,编译器会自动优化,如果定义 )编译器会自动优化,代码很长或者递归编译器可能就不会展开,忽略掉内联
定义在类内的成员函数默认定义为内联函数
为什么声明和定义要在两个文件中?
1,方便约定
2.更深层次的封装,闭源
3.提高编译速度、
比较小的函数直接在类中写出来
宏的优缺点
宏:增强程序的可维护性,在编译时展开
缺点:没有类型安全的检测,不方便调试,可读性差
尽量以const,enum,inline替换#define
注意:在C++中,强制建议使用const代替宏常量,使用内联函数代替宏函数。const和内联在进行编译时不仅进行替换,而且还会进行参数类型检查,提高了程序的安全性。内联函数可以是普通函数,也可以是类的成员函数;函数式宏不可以作为类的成员函数
友元(friend) 优点:提高了程序的运行效率
友元函数在类中声明
友元函数可以直接访问类的私有成员变量,它是定义在类外的普通函数,不属于任何类,但需要在类的内部进行声明
说明:
1.友元函数可以访问类的私有成员,但不是类的成员函数
2.友元函数不能在函数名后用const修饰,函数名前加const可以,表示返回值是常量
3.友元函数可以在类定义的任何地方声明,不受类访问限制符限制
private:
int _year;
int _month;
int _day;
friend ostream& operator<<(ostream& out, const Date& d);
}; 声明在私有成员下面也可以正常调用
4.一个函数可以是多个类的友元函数
5.友元函数的调用与普通函数的调用和原理相同
友元类:???
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问一个类中的非公有成员
cout ostream类中的对象
//d.operator<<(cout) out也是一个类
void operator<<(ostream& out)
{
out << _year << "-" << _month << endl;
}
d1.operator<<(cout);
d1 << cout; 这样调用很奇怪
void operator<<(ostream& out, const Date&d)
{
out << d._year << "-" << d._month << endl;
}
friend void operator<<(ostream& out, const Date&d);
operator<<(cout,d1);
cout<<d1 ; 这样不能连续调用也不对
friend ostream& operator<<(ostream& out, const Date&d);
ostream& operator<<(ostream& out, const Date&d)
{
out << d._year << "-" << d._month << endl;
return out;
}
cout << d1 << endl;
cout << d1; 终极版本
istream& operator >>(istream& in, Date& d)
{
in>> d._year;
in>> d._month;
return in;
} cin >> d1;
输入输出必须用友元,友元破坏了封装性,不宜多用
注意:
友元关系不能继承。友元关系是单向的,不具有交换性。友元关系不能传递
只有类的成员函数才能在函数名后面加上const,这时成员函数叫做常量成员函数
用意是不能通过this指针修改类对象成员变量的值、
含义:const修饰的是类函数隐藏的第一个参数 this指针,这表明this指针只读,也即类成员不可修改
注意该用法只能是成员函数,要是类的静态函数或者是非成员函数就不可以在函数名后面加上const
1) 函数名前加const
const int Func() {}
含义:返回值不可修改
静态成员(static)
静态成员变量在类外初始化,在类中声明加static,定义时不用加关键字
静态成员的调用 类名::静态成员 对象.静态成员
静态成员为所有类对象所共享,不属于某个具体的实例
在main函数之前已经初始化
通过函数可以get值,但不属于某个对象的东西
生命周期长
static修饰成员函数没有this指针,只能调用静态成员变量,可以用const修饰
静态成员变量不占类的大小,放在全局变量区
非静态的成员函数可以调用静态成员
静态成员变量可以通过静态成员函数来调用
class Date
{
public:
Date()
{
count++;
}
Date(int year,int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
count++;
}
Date(const Date& d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
count++;
}
static void print()
{
cout << Date::count << endl;
}
private:
int _year;
int _month;
int _day;
static int count;
};
int Date::count = 0;
int main()
{
Date d;
Date d1(2018, 8, 2);
Date d2 = d1;
Date::print();
}
编译器的优化(调用了几次构造,拷贝构造,赋值,析构)3种情况
1.传参
传值void Fun(Date d){} 1次构造-->1次拷贝构造-->2次析构
传引用void Fun(Date& d){} 1次构造,1次析构
2.传返回值
Date fun2()
{
Date d;
return d; return Date();-->调用了一次构造,一次析构
} 1次构造-->1次拷贝构造-->2次析构
匿名对象在构造的下一步就销毁
Date();执行完这条语句就销毁
void Fun(Date d) Fun(Date())调用了1次构造,一次析构,编译器自动优化,将两步合二为一
只有在一个表达式中,编译器才优化
Date fun2()
{
Date d; Date d;
return d; d=fun2();
} 2次构造-->1次拷贝构造-->1次析构-->1次赋值-->2次析构
Date d=fun2(); 这样的表达式编译器会优化 1次构造-->1次拷贝构造-->2次析构
如果以上语句,函数内部return Date() 1次构造-->1次析构
3.接收返回值
优化:两次拷贝构造合成一次
写了拷贝构造不写构造函数,会报错