Hello,C++(3)类和对象
类和对象
1、 基本概念
a)封装
概念:1、把属性和方法封装成类
2、对属性和方法进行访问控制
类成员的访问控制:C++中可以给成员变量和成员函数定义访问级别
Public修饰成员变量和成员函数可以在类的内部和类的外部被访问:完全公开
Private修饰成员变量和成员函数只能在类的内部被访问:基类访问
Protected只能用在继承里面:仅限于整个家族的访问
struct和class关键字区别:
用struct定义类时,所有成员的默认属性为public
用class定义类时,所有成员的默认属性为private
b)继承
之后专门整理
c)多态
2、 对象的构造和析构
创建一个对象时,常常需要作某些初始化的工作,例如对数据成员赋初值。注意,类的数据成员是不能在声明类时初始化的。
为了解决这个问题,C++编译器提供了构造函数(constructor)来处理对象的初始化。构造函数是一种特殊的成员函数,与其他成员函数不同,不需要用户来调用它,而是在建立对象时自动执行。
a)构造函数
i.构造函数的定义
1)C++中的类可以定义与类名相同的特殊成员函数,这种与类名相同的成员函数叫做构造函数;
2)构造函数在定义时可以有参数;
3)没有任何返回类型的声明。
ii. 构造函数的调用
自动调用:一般情况下C++编译器在创建对象时会自动调用构造函数
手动调用:在一些情况下则需要手工调用构造函数
注意:当类中提供了有参构造函数或无参构造函数,c++编译器都不会提供默认无参构造函数。也就是说,当你定义了构造函数,你必须使用它。
iii.构造函数的分类使用
1) 无参数构造函数
调用方法: Test t1, t2;
2) 有参构造函数
Test5 t1(10); //c++编译器默认调用有参构造函数 括号法
Test5 t2 = (20, 10); //c++编译器默认调用有参构造函数 等号法
Test5 t3 = Test5(30); //程序员手工调用构造函数 产生了一个对象 构造函数法赋值法
3)拷贝构造函数
使用某个对象的实例来初始化这个对象的一个新的实例,用于类对象之间的复制
如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。
拷贝构造函数的最常见形式如下:
AA(const AA &obj) //obj 是一个对象引用,该对象是用于初始化另一个对象的。
实例 :
#include <iostream> using namespace std; class A { private: int a; public: A(int i){a=i;} //内联的构造函数 A(A &aa); int geta(){return a;} }; A::A(A &aa) //拷贝构造函数 { a=aa.a; cout<<"拷贝构造函数执行!"<<endl; } int get_a(A aa) //③参数是对象,是值传递,会调用拷贝构造函数 { return aa.geta(); } int get_a_1(A &aa) //如果参数是引用类型,本身就是引用传递,所以不会调用拷贝构造函数 { return aa.geta(); } A get_A() //④返回值是对象类型,会调用拷贝构造函数,因为函数体内生成的对象aa是临时的,离开这个函数就消失了。所有会调用拷贝构造函数复制一份。 { A aa(1); return aa; } A& get_A_1() //会调用拷贝构造函数,因为函数体内生成的对象aa是临时的,离开这个函数就消失了。所有会调用拷贝构造函数复制一份。 { A aa(1); return aa; } int _tmain(int argc, _TCHAR* argv[]) { A a1(1); //调用内联构造函数 A b1(a1); //①用a1初始化b1,调用拷贝构造函数 A c1=a1; //②用a1初始化c1,调用拷贝构造函数 int i=get_a(a1); //③函数形参是类的对象,调用拷贝构造函数 int j=get_a_1(a1); //函数形参类型是引用,不调用拷贝构造函数 A d1=get_A(); //调用拷贝构造函数 A e1=get_A_1(); //调用拷贝构造函数 return 0; }
调用拷贝构造函数的四种时机:
1、A b1(a1); //①用a1初始化b1,调用拷贝构造函数
2、A c1=a1; //②用a1初始化c1,调用拷贝构造函数
3、int i=get_a(a1); //③函数形参是类的对象,调用拷贝构造函数
4、A get_A() //④函数的返回类型是对象
注意:
int get_a_1(A &aa) //如果参数是引用类型,本身就是引用传递,所以不会调用拷贝构造函数
小结:
必须定义拷贝构造函数的情况:
只包含类类型成员或内置类型(但不是指针类型)成员的类,无须显式地定义拷贝构造函数也可以拷贝;有的类有一个数据成员是指针,或者是有成员表示在构造函数中分配的其他资源,这两种情况下都必须定义拷贝构造函数。
什么情况使用拷贝构造函数:
当用一个已初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用。也就是说,当类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数:
① 程序中需要新建立一个对象,并用另一个同类的对象对它初始化。
② 当函数的参数为类的对象时。
在调用函数时需要将实参对象完整地传递给形参,也就是需要建立一个实参的拷贝,这就是按实参复制一个形参,系统是通过调用复制构造函数来实现的,这样能保证形参具有和实参完全相同的值。
③ 函数的返回值是类的对象。
在函数调用完毕将返回值带回函数调用处时。此时需要将函数中的对象复制一个临时对象并传给该函数的调用处。
如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的浅拷贝。
自定义拷贝构造函数是一种良好的编程风格,它可以阻止编译器形成默认的拷贝构造函数,提高源码效率。
b)析构函数
语法:~ClassName()
i.析构函数的定义
1)C++中的类可以定义一个特殊的成员函数清理对象,这个特殊的成员函数叫做析构函数
2)析构函数没有参数也没有任何返回类型的声明
3)析构函数在对象销毁时自动被调用
ii.析构函数的调用
C++编译器自动调用
构造函数和析构函数的调用顺序: 1)当类中有成员变量是其它类的对象时,首先调用成员变量的构造函数,调用顺序与声明顺序相同;之后调用自身类的构造函数 2)析构函数的调用顺序与对应的构造函数调用顺序相反
iii.深拷贝和浅拷贝
深拷贝和浅拷贝的定义可以简单理解成:如果一个类拥有资源(堆,或者是其它系统资源),当这个类的对象的资源发生复制过程的时候,这个过程就可以叫做深拷贝,反之对象存在资源,但复制过程并未复制资源的情况视为浅拷贝。
浅拷贝在释放资源的时候会产生资源归属不清的情况导致程序运行出错,例如:
在某些状况下,类内成员变量需要动态开辟堆内存,如果实行浅拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
浅拷贝和深拷贝的比较:
浅拷贝是指在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员,那么浅拷贝就会出问题了。
当调用一次构造函数,调用两次析构函数,而两个对象的指针成员所指内存相同,这会导致什么问题呢?指针被分配一次内存,但是程序结束时该内存却被释放了两次,会导致崩溃。
对含有指针成员的对象进行拷贝时,必须要自己定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏发生。
(以上为浅拷贝和深拷贝的意义,详细见https://blog.csdn.net/caoshangpa/article/details/79226270)
iv.初始化列表
1、必须使用初始化列表的几种情况:
1) 类成员为const类型
2) 类成员为引用类型
实例:
#include <iostream> using namespace std; class A { public: A(int &v) : i(v), p(v), j(v) {} void print_val() { cout << "hello:" << i << " " << j << endl;} private: const int i; int p; int &j; }; int main(int argc ,char **argv) { int pp = 45; A b(pp); b.print_val(); }
因为const对象或引用只能初始化但是不能赋值。构造函数的函数体内只能做赋值而不是初始化,
因此初始化const对象或引用的唯一机会是构造函数函数体之前的初始化列表中。
3) 类成员为没有默认构造函数的类类型
#include <iostream> using namespace std; class Base { public: Base(int a) : val(a) {} private: int val; }; class A { public: A(int v) : p(v), b(v) {} void print_val() { cout << "hello:" << p << endl;} private: int p; Base b; }; int main(int argc ,char **argv) { int pp = 45; A b(pp); b.print_val(); }
原因同样是创建对象时,要初始类成员的每一个成员(如果没有在初始化列表里面,编译器会自动使用它的默认的构造函数进行初始化,
但是它没有默认构造函数,所以会编译报错,所以没有默认构造函数的成员变量需要使用初始化列表进行初始化)
4) 如果类存在继承关系,派生类必须在其初始化列表中调用基类的构造函数
#include <iostream> using namespace std; class Base { public: Base(int a) : val(a) {} private: int val; }; class A : public Base { public: A(int v) : p(v), Base(v) {} void print_val() { cout << "hello:" << p << endl;} private: int p; }; int main(int argc ,char **argv) { int pp = 45; A b(pp); b.print_val(); }
2、语法规则
Constructor::Contructor() : m1(v1), m2(v1,v2), m3(v3) { // some other assignment operation }
3、注意
初始化:被初始化的对象正在创建;
赋值:被赋值的对象已经存在;
成员变量的初始化顺序与声明的顺序相关,与在初始化列表中的顺序无关。
v.对象的动态建立和释放
在软件开发过程中,常常需要动态地分配和撤销内存空间,例如对动态链表中结点的插入与删除。在C语言中是利用库函数malloc和free来分配和撤销内存空间的。C++提供了较简便而功能较强的运算符new和delete来取代malloc和free函数。
注意: new和delete是运算符,不是函数,因此执行效率高。
new int; //开辟一个存放整数的存储空间,返回一个指向该存储空间的地址(即指针) new int(100); //开辟一个存放整数的空间,并指定该整数的初值为100,返回一个指向该存储空间的地址 new char[10]; //开辟一个存放字符数组(包括10个元素)的空间,返回首元素的地址 new int[5][4]; //开辟一个存放二维整型数组(大小为5*4)的空间,返回首元素的地址
new和delete运算符使用的一般格式为:
应用举例:
类对象的动态建立和释放:
比如:
Box *pt; //定义一个指向Box类对象的指针变量pt pt=new Box; //在pt中存放了新建对象的起始地址 在程序中就可以通过pt访问这个新建的对象。如 cout<<pt->height; //输出该对象的height成员 cout<<pt->volume( ); //调用该对象的volume函数,计算并输出体积 C++还允许在执行new时,对新建立的对象进行初始化。如 Box *pt=new Box(12,15,18); 这种写法是把上面两个语句(定义指针变量和用new建立新对象)合并为一个语句,并指定初值。这样更精炼。 新对象中的height,width和length分别获得初值12,15,18。调用对象既可以通过对象名,也可以通过指针。
3、静态成员变量成员函数
有没有一些属性,归所有对象拥有?答案是静态成员变量。
1)静态成员变量
- 关键字 static 可以用于说明一个类的成员,
- 静态成员提供了一个同类对象的共享机制
- 把一个类的成员说明为 static 时,这个类无论有多少个对象被创建,这些对象共享这个 static 成员
- 静态成员局部于类,它不是对象成员
例如: #include<iostream> using namespace std; class counter { static int num ; //声明与定义静态数据成员 public : void setnum ( int i ) { num = i ; } //成员函数访问静态数据成员 void shownum() { cout << num << '\t' ; } } ; int counter :: num = 0 ;//声明与定义静态数据成员**************必须在类外声明和定义******************* void main () { counter a , b ; a.shownum() ; //调用成员函数访问私有静态数据成员 b.shownum() ; a.setnum(10) ; a.shownum() ; b.shownum() ; }
2)静态成员函数
- 静态成员函数数冠以关键字static
- 静态成员函数没有this指针
- 在类外调用静态成员函数用 “类名 :: ”作限定词,或通过对象调用
4、this指针
C++中类的普通成员函数都隐式包含一个指向当前对象的this指针
实验1:若类成员函数的形参 和 类的属性,名字相同,通过this指针来解决。
实验2:类的成员函数可通过const修饰,请问const修饰的是谁 ?
①Void ② fun(int a,int b)③
1、 const放在123位置都可以
2、 const修饰的是this指针指向的内存空间,只读属性
3、 const修饰的不是指针,而是指针指向的内存空间
4、 this指针默认为const类型,指针不能被修改,再加一个const表示内存空间不能被修改
5、全局函数和成员函数的对比
1、把全局函数转化成成员函数,通过this指针隐藏左操作数
Test add(Test &t1, Test &t2)===》Test add( Test &t2)
2、把成员函数转换成全局函数,多了一个参数
void printAB()===》void printAB(Test *pthis)
3、函数返回元素和返回引用
下面的函数要实现 t1=t1+t2
成员函数可以返回自身,而全局函数需要重新定义t3返回。
Test& add(Test &t2) //*this //函数返回引用
{
this->a = this->a + t2.getA();
this->b = this->b + t2.getB();
return *this; //*操作让this指针回到元素状态
}
Test add2(Test &t2) //*this //函数返回元素
{
//t3是局部变量
Test t3(this->a + t2.getA(), this->b + t2.getB());
return t3;
}
void add3(Test &t2) //*this //函数返回元素
{
//t3是局部变量
Test t3(this->a + t2.getA(), this->b + t2.getB());
//return t3;
}
6、友元
a)友元函数
友元函数是类的好朋友,在友元函数里可以修改类的私有属性破坏类的封装性
友元函数是全局函数所以需要包含参数类名和变量名
友元函数声明的属性public private都可以
实例1:
class A1
{
public:
A1()
{
a1 = 100;
a2 = 200;
}
int getA1()
{
return this->a1;
}
//声明一个友元函数
friend void setA1(A1 *p, int a1); //这个函数是这个类的好朋友
protected:
private:
int a1;
int a2;
};
void setA1(A1 *p, int a1)
{
p->a1 = a1;
}
void main()
{
A1 mya1;
cout<<mya1.getA1()<<endl;
setA1(&mya1, 300); //通过友元函数 修改A类的私有属性
cout<<mya1.getA1()<<endl;
system("pause");
}
实例2:
b)友元类
概念:B类是A类的好朋友,B类里面组合了一个A的对象,通过B可以直接修改A
- 友员类通常设计为一种对数据操作或类之间传递消息的辅助类
- 若B类是A类的友员类,则B类的所有成员函数都是A类的友员函数
(B类里面所有的成员函数都可以改变A的私有成员变量,B是改变的中介,相当于一个陷门)
7、运算符重载
重载的概念:一个函数名可以用来代表不同功能的函数
例如:Complex c3 = c1 + c2; //用户自定义类型 编译器无法让变量相加,需要重载+运算符
运算符重载的本质是一个函数
a)实现方法
- 重载函数原型为:
类型 类名 :: operator= ( const 类名 ) ;
步骤:
1、定义一个全局函数
2、 函数名升级(把函数名变成operate+、改写成成员函数)
但是熟练之后在项目里一般先写函数调用,然后根据函数调用去写函数原型。
//全局函数 完成 +操作符 重载
Complex operator+(Complex &c1, Complex &c2)
//类成员函数 完成 -操作符 重载
Complex operator-(Complex &c2)
b)限制
c)友元函数实现操作符重载
//前置++操作符 用全局函数实现 Complex& operator++(Complex &c1) { c1.a ++; c1.b ++; return c1; } //调用方法 ++c1 ; //需要写出操作符重载函数原形 c1.printCom(); **********************************//前置—操作符 成员函数实现 Complex& operator--() { this->a--; this->b--; return *this; } //4.2调用方法 --c1; c1.printCom(); //4.3前置—运算符重载函数名定义 //c1.operator--() ********************************** //5.1 后置++ 操作符 用全局函数实现 Complex operator++(Complex &c1, int) //由于前置++和后置++的操作符重载相同,编译器会报错,所以出现了占位符语法,后置++会在函数后面加一个占位符 { Complex tmp = c1; c1.a++; c1.b++; return tmp; } //5.2 调用方法 c1 ++ ; //先使用 后++ //5.3 后置++运算符重载函数名定义 Complex operator++(Complex &c1, int) //函数占位参数 和 前置++ 相区别 ********************************* //6.1 后置— 操作符 用类成员函数实现 Complex operator--(int) { Complex tmp = *this; this->a--; this->b--; return tmp; } //6.2 调用方法 c1 ++ ; //先使用 后++ //6.3 后置--运算符重载函数名定义 Complex operator--(int) //函数占位参数 和 前置-- 相区别
前置和后置运算符总结
C++中通过一个占位参数来区分前置运算和后置运算
全局函数、类成员函数方法实现运算符重载步骤
1)要承认操作符重载是一个函数,写出函数名称operator+ ()
2)根据操作数,写出函数参数
3)根据业务,完善函数返回值(看函数是返回引用 还是指针 元素),及实现函数业务
注意:
输入输出流的运算符重载,只能使用友元函数的方法实现
小结:
- 类通常用关键字class定义。类是数据成员和成员函数的封装。类的实例称为对象。
- 结构类型用关键字struct定义,是由不同类型数据组成的数据类型。
- 类成员由private, protected, public决定访问特性。public成员集称为接口。
- 构造函数在创建和初始化对象时自动调用。析构函数则在对象作用域结束时自动调用。
- 重载构造函数和复制构造函数提供了创建对象的不同初始化方式。
- 静态成员是局部于类的成员,提供一种同类对象的共享机制。
- 友员用关键字friend声明。友员是对类操作的一种辅助手段。一个类的友员可以访问该类各种性质的成员。
-
操作符重载的本质是通过函数扩展操作符的语义
-
operator关键字是操作符重载的关键
-
friend关键字可以对函数或类开发访问权限
-
=, [], ()和->操作符只能通过成员函数进行重载
-
++操作符通过一个int参数进行前置与后置的重载
-
C++中不要重载&&和||操作符