2.1 面向对象
对象即是现实世界中某个具体的物理实体在计算机逻辑中的映射和体现
类 class :是一组相关的属性(变量)与行为(方法)的集合,是一个抽象概念设计的产物。
c++中,类是一种数据类型
成员变量是对象的属性,属性的值确定对象的状态。
成员函数是对象的方法,确定对象的行为。
面向对象三大特性:封装 继承 多态 (抽象)
类的设计:
//class为数据类型说明符
class student
{
public:
bool sex;
void set_age(int age);
private:
int Age;
char name[10];
};
//对象方法的类外声明(此时函数明与参数列表需相同)
void student::set_age(int age)
{
Age = age;
}
int main()
{
student c1;//创建对象
c1.sex = 0;//合法
c1.Age = 10;//错误,不可访问private成员
}
如成员可见性不进行设置,默认为private
private与protected体现了类具有封装性
一般将变量设置为private,方法设置为public;
这样,就仅可靠对外的接口来改变变量的值
注意,空的类大小为1而非0(用来让系统标识类在系统空间中的位置)
在创建多个对象时,不同对象的属性存放于不同区域;全局对象存放于数据区,在函数内部定义则为栈区,用new构建则为堆区
而方法只在代码区创建一次,同个对象的不同实例共用
在方法调用时,系统为分辨调动的主体–因而引入this指针的概念:
2.2 this 指针
编译器对类型的编译为以下三步:
1.识别函数属性
2.函数原型(非函数体部分)的识别
3.改写
如在上例的void studvent::set_age(int age) 会被改写为void studvent::set_age(stuent* const this,int age) ,这样,在多个实例调用方法时会再进行以下改写
int main()
{
student s1,s2;
s1.set_age(2);//改写为set_age(&s1,2);
s2.set_age(3);//改写为set_age(&s2,2);
}
在c++默认的调用约定thiscall下,this指针的值是通过ecx来传递的,如在s1.set_age(2)中,首先先将s1的地址[s1]传递给ecxlea ecx,[s1],之后进入函数内部,ecx中的值传递给this指针 mov [this],ecx/如使用c的调用约定cdecl,则是以入栈的形式实现push eax/
(在编译时自动入参)
1.是指向本对象的指针;其存在于成员函数内,而非对象内,只有在调用成员方法时产生this指针
2.普通成员方法的第一个参数,默认加上this指针;
3.在普通成员方法内只用到普通成员的地方,加上this指针的解引用 this->
4.在调用成员方法时,加上参数this指针
const与成员方法
如对于成员函数 int get_num()在其之前增加constconst int get_num()是说明该函数返回值为常量;
在其之后增加constint get_num() const则是声明此方法为常方法,常方法内只能对对象进行读取,而无法写入(即无法修改)
注意,常对象如const student s1调用普通方法会报错——普通方法的this指针(student * const this)无法指向常对象(能力扩展)。因此,常对象只可指向常方法。/·普通方法不受限制·/
2.3构造函数
当所有成员属性可见性皆为public时,可直接用{···}对对象进行初始化:
class pointer
{
public:
int row;
int col;
};
int main()
{
pointer p = {12,23};
}
一个对象的数据成员多为私有的,要对它们进行初始化,必须用一个公有函数来进行。同时这个函数应该在且仅在定义对象时自动执行一次。称为构造函数(constructor) 。
构造函数的用途:1)创建对象,2)初始化对象中的属性,3)类型转换。
初始化列表
只有构造函数有初始化列表
必须初始化的成员放在初始化列表
在本对象构造之前需要完成的动作必须放在初始化列表中
从const 成员 必须放在初始化列表中
const方法
常对象只能调用常方法(this指针不匹配) – 构造,析构,重载不受影响
常方法中只能调用常方法 – 静态函数不影响
class Int
{
private:
int val;
public:
Int(){} //缺省构造函数
Int(int x) //构造函数重载
{
val= x;
}
};
int main()
{
Int p1(1); // 创建对象并初始化
Int p2; //调用缺省构造函数
Int p3(); //无法构造对象,系统理解为函数声明
p2.Int(1,1) // 错误,构造函数仅可由系统调用,对象无法调用
int b = 2;
p1 = b; // 类型转化(隐式转换)
}
构造函数的类型转化
类型转化只适用于单参的构造函数
上例中,系统先b的值创建一个临时对象,然后将临时对象的值赋给p1;
要防止隐式转换可以通过在构造函数前添加explicit关键字实现,此时实现类型转换需要用强制类型转换(也称显式转换),如 p1 = (Int)b
构造函数使用时–
1.对象进行构造时时默认调用的函数,在对象生存周期内(由系统)只调用一次
2.函数名与类名一致
3.构造函数无函数返回类型说明;但实际上构造函数有返回值,为其创建的对象
4.可重载
5.未定义时,系统默认生成一个默认构造函数(除 this 指针外,没有参数的构造函数)
拷贝构造函数
1.当使用一个已存在的对象为另一个对象赋值时,自动调用的成员方法
2.如果自己不实现,自动生成一个浅拷贝的等号运算符重载函数
3.防止自赋值
4.防止内存泄漏
5.防止浅拷贝
在建立对象时可用同一类的另一个对象来初始化该对象的存储空间,这时所用的构造函数称为拷贝构造函数(Copy Constructor).
这个拷贝过程只需要拷贝数据成员,函数成员是共用的。
class Object
{
int value ;
public:
object (int x) : valuc(x) {}
~object() {}
//拷贝构造函数
//使用引用的原因——防止死递归
//无返回值
//也可让参数为常对象引用
object(object& obj):value(obj.val)
{}
}
//下例中将构造5次对象
object fun(object obj) //3
{
int val = obj.Getvalue();
object obja(val); //4
return obja; //5 构建临时对象——调用拷贝构造
}
int main(
{
object obja(10) ;//1
object objb(0); //2 调用构造函数
objy = fun(obja);
return 0 ;
}
为避免函数的多次构建,可以将参数改为引用形式——即object fun(object& obj) ;
有时,为了避免obj对象被错误修改,也可以加上const关键字—object fun(const object& obj)
1.用一个已存在的对象为另一个正在生成的对象初始化的时候自动调用的成员方法;
2.应当预防浅拷贝
3.如果未自己实现,则生成一个浅拷贝的拷贝构造函数
4.使用引用;以防死递归(重复创建新对象)
总结:
拷贝构造函数的参数——采用引用。如果把一个真实的类对象作为参数传递到拷贝构造函数,会引起无穷递归。在类中如果没有显式给出拷贝构造函数时,则C++编译器自动给出一个缺省的拷贝构造f函数。如果有程序设计者定义的构造函数(包括拷贝构造函数),则按函数重载的规律,调用合适的构造函数。
浅拷贝 – 直接为指针赋值 (不常用)
深拷贝 – 重新申请内存并将数据传入
2.4析构函数
当一个对象的生命周期结束时,C++会自动调用一个成员函数注销该对象,这个成员函数叫做**析构函数(destructor) **
析构函数–
1.对象生存周期满之后系统会自动调用,也可由对象主动调动
2.~ + 对象名;如person类的析构函数为 ~person()
3.在栈帧中,先构造的函数后析构;
4.未实现时,调用默认析构函数(该函数什么都不做)
5.一个类中只有一个析构函数
6.析构函数无函数返回类型说明;是否有返回值取决于编译器
友元
为了在类外访问其内部成员,将要注册的函数/成员/类前添加friend关键字并将其声明添加进类内,使其可访问类的内部成员
友元不具有自反性
友元不具有传递性
友元不具有继承性
实现方式
1.函数友元
class Object
{
private:
int value;
public:
0bject(int x) :value(x){}
friend ostream& operator<<(ostream& out,const Object& obj) //声明为友元函数
};
ostream& operator<<(ostream& out,const Object& obj)
{
out << obj.value; //友元函数可以访问类的private成员
return out;
}
int main()
{
Object obja(10);
cout<< obja << endl;
}
2.成员函数友元
class Object; //构造base前需要对object对象声明
class Base
{
private:
int sum;
public:
Base(int x = 0): sum(x){}
void fun (Object&);
};
class 0bject
{
private:
int value;
public:
0bject(int x) : value(x)0
friend void Base::fun(Object& obj){};//base的成员函数声明为友元函数
};
void Base::fun(Object& obj)
{
obj.value = obj.value + sum;
}
3.类友元
class Object; //构造base前需要对object对象声明
class Base
{
friend class Object; //Object注册为Base的友元函数
private:
int sum;
public:
Base(int x = 0): sum(x){}
void fun (Object&);
void print(Object&);
};
class 0bject
{
friend class Base; //Base注册为Object的友元类,base类可以访问Object的成员
private:
int value;
public:
0bject(int x) : value(x)0
};
//函数的类外实现
void Base::fun(Object& obj)
{
···
}
void Base::print(Object& obj)
{
···
}
=运算符重载
在运算符前添加关键词operator
class person
{
public:
int age;
int sex;
person()
{
_age = src._age;
_sex = src.sex;
}
//运算符重载
//以引用返回,因为this的生命周期大于operator=,可以以引用方式返回,从而避免了构建临时对象
person& operator= (const person& src) const
{
// 防止自符值
if (this != &src)
{
this->age = src.age;
}
//返回*this,可以实现连续赋值
//如person1 = per2 = per3
return *this ;
}
};
int main()
{
person p1(32,1);
person p4;
p4 = p2;//编译如下:p4 = p4.operator(p2);
// operator(&p2,p2);
return 0;
}
()运算符重载
对类型转换符()的重载,能使得对某些class的操作更加方便。
例如:
class Int
{
private:
int val;
public:
Int(int x):val(x){}
operator int() const
{
return val;
}
};
int main()
{
Int a(1),b(2);
a < b; //系统此处调用()
int x = 100;
a < 100; //同上
}
例2:
class Add
{
//易变关键字mutable,使得声明变量在常方法中也可以被改变
mutable int value;
public:
Add(int x = 0):value(x){}
itn operator()(int a,int b) const
{
value = a + b;
return value;
}
}
int main ()
{
int a = 10,b = 20,c = 0;
Add add;
c = add (a,b);//仿函数
//无异于c = add.operator()(a,b);
return 0;
}
缺省函数
注意,当以下函数未自行构建时,系统会自动提供缺省函数
例:
class object
{
private:
int num;
int ar[5]:
public:
object (int n,int val = 0):num(n)
{
for(int i - 0: i < n; ++i)
{
ar[i] = val;
}
}
};
int main()
{
object obja(5,23);
object objb(obja);
object objc(5);
objc = obja;
}
自建的拷贝与赋值重载如下
//拷贝
object (const object& obj):num(obj.num)
{
for(int i = 0: i < 5; ++i)
{
ar[i] = obj.ar[i];
}
}
//赋值
object& operator=(const object& obj)
{
if(this != &obj)
{
num = obj.num;
for(int i = 0: i < 5; ++i)
{
ar[i] = obj.ar[i];
}
}
return* this;
}
注意:如果未自己构建,且赋值的类为简单类型,系统所构建的缺省函数不会有函数调用过程,而只是简单的赋值。
简单类型:不具有继承关系,不具有虚函数,成员均为基本数据类型
如在 objc = obja中,系统只是获取obja的地址,并将其中的数据依次赋值给objc中对应的空间
例2:
class Object
{
int num ;
int* ip;
public:
Object(int n,int val = 0):num(n)
{
ip = (int*)malloc(sizcof(int) *n) ;
for (int i = 0; i < num; ++i)
{
ip[i] = val ;
}
}
~Objcct()
{
free(ip);
ip = NULL;
)
};
int main()
{
Object obja(5,23);
Object objb(obja);
Object objc(8);
objc = obja ;
}
在该例中,执行 objc = obja,若未构建赋值运算符重载函数,系统会将objc中的num赋值以obja中的num;同时,objc中的ip指针则会被赋值为obja中ip的值(即两个指针指向同一片空间),这并不是我们想要的结果。
因此需要自建赋值运算符重载函数
自建的拷贝与赋值重载如下
//拷贝
object (const object& obj):num(obj.num)
{
ip = (int*)malloc(sizcof(int)*num) ;
for (int i = 0; i < num; ++i)
{
ip[i] = obj.ip[i];
}
}
//赋值
object& operator=(const object& obj)
{
if(this != &obj)
{
num = obj.num;
ip = (int*)malloc(sizcof(int)*num) ;
for (int i = 0; i < num; ++i)
{
ip[i] = obj.ip[i];
}
}
return* this;
}
小结:
1.运算符重载函数的函数名必须为关键字operator加一个合法的运算符。在调用该函数时,将右操作数作为函数的实参。
2.当用类的成员函数实现运算符的重载时,运算符重载函数的参数(当为双目运算符时)为一个或(当为单目算符时)没有。运算符的左操作数一定是对象,因为重载的运算符是该对象的成员函数,而右操作数是该函数的参数。
3.单目运算符“++”和“- -”存在前置与后置问题。
前置“++”格式为:
class:operator++(){…}
而后置“++”格式为:
class:operator++(int){…}
后置“++”中的参数int仅用作区分,并无实际意义,可以给一个变量名,也可以不给变量名。
当返回值为自身(*this)时,函数重载以引用方式返回(如=,前置++,+=);返回值为临时量时,以值得形式返回(如+,后置++,)
2.5 C++中的权限
private私有的
类内部可使用,其他地方不可调用
不做设置时,class中权限默认为private
权限选择:对外界必须提供时,定义在public中;其他情况定义在private中;
成员属性定义在private,外部需调用时仅提供接口;为防止改动,接口定义时使用const
struct同样可定义一个类;其中默认权限为public
初始化 –
赋值 –
哪些成员方法写成常方法
1.如果成员方法内不要改动成员,并且没有对外暴露成员引用||指针,就可以写成常方法;
2.如果成员内部不需要改动成员,但是会对外暴露成员引用或是指针;则写成两个成员方法(const方法与非const方法)
3.如果成员方法内部需要改动成员,则写为普通方法
Person()
{
:_sex(1)//初始化列表
}
Person(const)
{
:_sex(1)//初始化列表
}
//此const修饰this指针;等效于 const * this
Person()const
{
:_sex(1)//初始化列表
}
静态成员变量与静态成员方法
静态量存储在数据段
一个类的不同实例共用一个静态成员,并且其不占用类实体的空间
需要在类外进行初始化
必须在类外的.cpp文件中初始化,且只能初始化一次(在.h文件初始化会使每次调用都会初始化)
静态成员方法访问不依赖this指针,
只能使用静态成员变量(因为其不依赖this指针)
派生类共享使用基类的静态成员变量
template<class T>
class 0bject
{
private:
//常见静态成员可见性设为private
static int num; //静态成员不能在类内初始化
int test=0;
public:
T value;
Object(T x = T()): value(x){}
//静态成员函数,static并非修饰其返回值,而是表面函数不使用this指针,因此该函数只可访问静态成员,并且此函数无法声明为const方法
static int add()
{
num++; //T,静态方法只可访问静态成员
test++;//F,无this指针无法访问非静态成员
}
};
template<class T>
int Object<T>::num = 0;//静态成员的类外初始化,注意,初始化语句并非函数内的执行语句,因此,即便 num可见性为private,该语句依然正常执行
class Base : public Object<int>
{
public:
Base()
{
num += 1;
}
void Print() const
{
num +=1; //static成员不依靠this指针访问,因此即便方法为常方法,num依旧可以被修改
cout << "Base:num:" <<num <<endl ;
}
};
class Test : public Object<double>
{
public:
Test()
{
num += 1;
}
void Print() const
{
cout << "Test: " <<num << endl;
}
};
int main()
{
Base b1,b2;
Test t1,t2;
b1.print(); //
t1.print(); //
}
例1:
class Object
{
public :
int value;
static Object obj;
};
注意,该class是可以编译通过的,static Object obj只有一份且存在于数据区,为各Object实体所共用,单独的Object实体内只含有value属性,所以不存在重复构建的问题
例2.1:static成员在单例模式中的应用
单例模式:创建对象时,只创建一个对象
class Object{
private:
int value;
static Object instance;
//构造函数设为私有,使其无法被外部函数调用
Object (int x = 0) : valuc(x){}
//将拷贝构造与=重载函数delete(弃用)
Object (const Object& obj) = delete;
Object& operator=(const Object& ob) = delete;
public:
//通过一个public函数提供接口来实现外部对类内唯一对象static Object instance的访问
//注意,只能以&的方式返回,因为值返回所使用的拷贝构造函数(返回时构建临时对象会调用拷贝构造函数)已经被delete
static Object& GotTnstance ()
{
return instance;
}
};
Object Object:: instance(10); //static 对象的类外初始化
int main()
{
Object obj(10); //F,外部函数不可调用构造函数
Object& obja = Object::GotTnstance();//T
Object& bojb = obja.GotTnstance(); //T,obja与objb所引用的是同一个对象
return 0;
}
此例中所实现的单例模式称为线程安全(对象构建时的线程安全)