2020.12.c++类和对象
c++面向对象
一.内存分析
1.内存分区模型
2.程序运行前
- 未执行程序前分为两个区域,代码区和全局区
- 代码区是共享的,其目的是对于频繁被执行的程序,只需要在内存中有一份代码就行了。
- 代码区是只读的,防止程序被意外地修改了
- 全局区存放全局变量(在int main()函数或者其他函数外面,定义的变量)、静态变量(变量前面+static的变量)、常量(字符串常量和全局常量量)
- 注意:局部变量和局部常量,即在int main()函数里面或者其他函数里面,定义的普通变量,不在全局区
3.程序运行后
- 栈区:由编译器自动分配和释放,存放函数的参数值(形参)、局部变量
- 注意:函数最后,不要返回局部变量的地址,因为栈区在程序运行后会释放。
- 堆区:由程序员分配释放,若程序员不释放,程序结束后由操作系统自己回收
- 注意:c++中主要利用new关键字开辟内存
4.new关键字
- new返回的是该数据类型的指针
int* p = new int(10);
int* arr=new int[10];//arr为数组首地址
- 可以利用delete释放空间,delete+指针名(delete p; delete[] arr)
- 释放数组时:要加一个中括号,代表释放数组
二.引用
1.给变量起别名
-
语法:数据类型 &别名=原名;
-
eg:
int a=10;
int &b=a;
//a=10,b=10
注意:程序修改原变量值或者别名变量的值,数据都会改变,因为别名和原名的地址是相同的
2.使用引用的注意事项
- 必须要初始化,且初始化后,就不可以更改。
int a=10;
int &b=a;
int c=20;
b=c;//赋值操作,不是修改别名,这里将20赋给b,a和b指向同一内存
//a=20,b=20,c=20
3.引用传递
//引用传递
#include <iostream>;
using namespace std;
void swap3(int& m, int& n) {
int temp = m;
m = n;
n = temp;
cout << "swap3:a =" << m << endl;
cout << "swap3:b =" << n << endl;
}
int main() {
int a = 10;
int b = 20;
//swap(a, b);
//swap2(&a,&b);
swap3(a, b);
cout<<"a ="<< a<<endl;
cout<<"b ="<< b<<endl;
}
/*swap3:a =20
swap3:b =10
a =20
b =10
*/
- 引用传递:形参也会改变实参,原因是&m&n分别是ab的别名,两个指向的是相同的地址
4.引用做函数的返回值
- 注意:函数最后,不要返回局部变量的引用,因为栈区在程序运行后会释放。
- 前面也说到,函数最后,不要返回局部变量的地址,因为栈区在程序运行后会释放。
- 函数引用调用可以作为左值存在
#include <iostream>
#include <String>
using namespace std;
//**注意:函数最后,不要返回局部变量的地址或者引用,因为栈区在程序运行后会释放。**
//***注意:函数引用调用可以作为左值存在
int& back1() {
int m = 10;
return m;
}
int& back2() {
static int m = 10;
return m;
}
int main() {
int& a = back1();
cout << a << endl;
cout << a << endl;//a是别名,也可以打印原名back1()
//第一次有值,因为编译器进行了保留,第二次乱码,因为栈区执行完该函数,释放了内存
int& b = back2();
cout << b << endl;
cout << b << endl;//b是别名,也可以打印原名back2()
back2() = 100;
cout << b << endl;
cout << b << endl;//b是别名,也可以打印原名back2()
}
/*10
2067384816
10
10
100
100
*/
5.引用的实质
- 引用的实质就是指针常量(指针的指向不可以改,但是指针的值可以改),这就解释了为什么引用不可以改。
int a =10;
int &b=a;//自动转化为int *const b=&a;
b=20;//,内部发现b是引用,自动帮我们转化为*b=20;
6.常量引用
-
用于修饰形参,防止误操作,修改了形参的值导致实参的值发生改变
void show(const int &val){ //val=100;报错,不能修改 cout<<"val= "<<val<<endl; } int main(){ int a=10; show(a); }
三.函数的加深
1.函数默认参数
- 定义语法:返回值类型 函数名(返回值类型 参数名(形参)=默认参数){}
- 注意1:在调用函数时,如果我们自己传入数据,就用自己数据,如果没有,就用默认值
- 注意2:在定义函数时,如果某个位置已有默认参数,那么从这个位置往后,都必须有默认参数
- 注意3:在声明函数时,如果有了默认参数,则在定义函数时,就不能有默认函数,声明函数和默认函数之间只能有一个有默认函数
#include <iostream>
using namespace std;
int add1(int a, int b, int c) {
return a + b + c;
}
int add2(int a, int b=10, int c=100) {
return a + b + c;
}
/*
int add2(int a, int b = 10, int c ) {
return a + b + c;
}函数定义,若有默认值,从左到右都必须有默认值,否则会报错!!!!!!!!!!!!!!!!
*/
int add3(int a, int b = 50, int c = 500);//函数声明
int add3(int a, int b , int c) {
return a + b + c;
}
/*
int add3(int a, int b = 50, int c = 500) {
return a + b + c;
}函数定义在再有默认值,会报错!!!!!!!!!!!!!!!!
*/
int main() {
cout << add1(1, 2, 3) << endl;
cout << add2(1)<<endl;
cout << add2(1,2)<<endl;//调用时,有值用调用值,没值用默认值
cout << add3(1)<<endl;
}
/*6
111
103
551
*/
2.函数占位参数
- 定义语法:返回值类型 函数名(返回值类型){}
- 占位参数不用写参数名,代表有一个位置,在调用参数时,()内需传入返回值对应的类型数据
- 注意:占位参数也可以传入默认参数
3.函数重载
-
1)函数重载满足的条件:同一作用域下,函数名称相同,函数参数列表不同(参数类型、个数、顺序不同等)
-
2)引用作为重载的条件
函数定义时,const 修饰形参和不修饰形参,是不同的函数,可以重载
-
3)函数重载遇到默认值
防止有歧义,少使用默认值
#include <iostream>
using namespace std;
//*****************************1.引用作为函数重载
//int &a=10;不合法
void printing(int& m) {
cout << "printing(int& m) " << endl;
}
//const int &a=10;合法
void printing(const int& m) {
cout << "printing(const int& m) " << endl;
}
//******************************2.当重载遇到默认值
int add(int a) {
return a ;
}
int add(int a, int b = 10) {
return a + b ;
}
int main() {
int a = 10;
printing(a);//运用的函数是 printing(int& m)
printing(10); //运用的函数是 printing(const int& m)
cout<<add(5, 10);
//add(10);有歧义,可以调用上面两个重载的函数
//只能尽量避免,重载函数少用默认值
}
/*printing(int& m)
printing(const int& m)
15
*/
四.类和对象
1.封装
-
1)概念:
将属性和行为作为一个整体,写在一起,表现为一个事务
-
2)语法
class 类名{ 访问权限:属性/行为};
创建对象----实例化:类名 具体名称;
class Student{
public:
//.....
};
int main(){
Student s1;//实例化
}
-
3)一些专有名词
类中的属性和成员我们统称为---成员
属性:成员属性,成员变量
行为:成员函数,成员方法
-
4)访问权限
类在设计时,可以把属性和行为放在不同权限下,加以控制
- public--->成员 类内可以访问 类外可以访问
- protected--->成员 类内可以访问 类外不可以访问,子类可以访问父类保护的内容
- private--->成员 类内可以访问 类外不可以访问,子类不可以访问父类私有的内容
-
5)结构体struct和类的区别class
- 默认访问权限不同
struct默认权限是 公共---public
class默认权限是 私有------private
- 调用函数、方法
struct不能自动调用方法
class能调用方法
-
6)成员属性设置为私有
- 可以有效控制读写权限
- 对于写权限而言,可以检测数据的有效性,如判断年龄不符合
#include <iostream> #include <String> using namespace std; class Student { public: void setName(string name) { s_name= name; } string getName() { return s_name; } void setAge(int age) { if (age < 0 || age>150) { s_age = 0; cout << "别骗人了!!!!" << endl; return ; } else { s_age = age ; } } int getAge() { return s_age; } string getSex() { s_sex = "男"; return s_sex; } //private访问的数据类外不能访问,所以就弄一个set/get函数得到他们 //从而实现读写权限 private: string s_name;//可读可写 int s_age;//可读可写 string s_sex;//可读 }; int main() { Student s1; s1.setName("aaa"); s1.setAge(1000); cout<<s1.getName()<<endl; cout<<s1.getAge()<<endl; cout<<s1.getSex()<<endl; } /*别骗人了!!!! aaa 0 男 */
2.对象的初始化和清理
- 1)基本概念
构造函数:主要作用在创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用
析构函数:主要作用在对象销毁前系统自动调用,执行一些清理工作
注意:构造函数和析构函数都是必须实现的,如果我们不提供,编译器会自己提供一个空实现的构造和析构,对于拷贝构造函数会是一个值拷贝
-
2)构造函数的分类及调用
-
按照参数分类:无参构造(默认构造)和有参构造
-
按照类型分类:普通构造和拷贝构造
-
构造函数的调用方法:括号法、显示法
-
#include <iostream>
using namespace std;
class Person {
public:
//!!!!!!!!!!!!!!!!!!!!构造函数!!!!!!!!!!!!!!!!
Person() {
cout<<"Person的无参构造函数"<<endl;
}
Person(int a) {
age = a;
cout << "Person的有参构造函数" << endl;
}
//将传入的人身上的所有属性拷贝到我(新的人)身上
Person(const Person &a) {
age = a.age;
cout << "Person的拷贝构造函数" << endl;
}
//除了这种是拷贝构造,其余均是普通构造
//!!!!!!!!!!!!!!!!!!!!析构函数!!!!!!!!!!!!!!!!!
~Person() {
cout << "Person的析构函数调用" << endl;
}
int age;
};
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!调用!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
void show() {
//1.括号法
Person p1;
Person p2(10);
Person p3(p2);
//2.显示法
Person p4 = Person(20);
//Person(20)是匿名对象,没有名字,当前行执行结束后,系统会立即收到匿名对象
Person p5 = Person(p4);
}
int main() {
show();
}
/*Person的无参构造函数
Person的有参构造函数
Person的拷贝构造函数
Person的有参构造函数
Person的拷贝构造函数
Person的析构函数调用
Person的析构函数调用
Person的析构函数调用
Person的析构函数调用
Person的析构函数调用
*/
-
3)拷贝构造函数的使用
- 使用一个已经创建完毕的对象来初始化一个对象
- 值传递的方式给函数参数传值
- 值方式返回局部对象(存疑,不懂)
//拷贝构造函数的使用 #include <iostream> using namespace std; class Person { public: Person() { cout << "Person的无参构造函数" << endl; } Person(int a) { age = a; cout << "Person的有参构造函数" << endl; } //将传入的人身上的所有属性拷贝到我(新的人)身上 Person(const Person& a) { age = a.age; cout << "Person的拷贝构造函数" << endl; } ~Person() { cout << "Person的析构函数调用" << endl; } int age; }; //使用一个已经创建完毕的对象来初始化一个对象 void show1() { Person p1(10); Person p2(p1); cout << p2.age << endl; } //值传递的方式给函数参数传值 void work(Person p) { } void show2() { Person p; work(p);//实际参数传给形式参数,调用拷贝构造函数 } int main() { //show1(); show2(); } /*show1():Person的有参构造函数 Person的拷贝构造函数 10 Person的析构函数调用 Person的析构函数调用 show2():Person的无参构造函数 Person的拷贝构造函数 Person的析构函数调用 Person的析构函数调用 */
-
4)构造函数的调用规则
- 如果用户定义了有参构造。c++不再提供无参构造,但是还是会提供默认的拷贝构造
- 如果用户定义了拷贝函数,c++不会再提供其他构造函数
-
5)浅拷贝和深拷贝
-
- 深拷贝:自己实现拷贝构造函数解决浅拷贝带来的问题(堆区的内存重复释放)
在堆区重新申请空间,进行拷贝操作
- 浅拷贝:利用编译器自己默认提供的拷贝构造函数
//深拷贝与浅拷贝
#include <iostream>
using namespace std;
class Person {
public:
Person() {
cout << "Person的无参构造函数" << endl;
}
Person(int age,int height){
p_age = age;
p_height = new int(height);//堆区的数据,需要自己去释放
cout << "Person的有参构造函数" << endl;
}
//自己实现拷贝构造函数,解决浅拷贝带来的问题
Person(const Person& a) {
p_age = a.p_age;
//p_height=a.p_age;编译器默认实现的这行代码
//深拷贝操作
p_height = new int(*a.p_height);//重新在堆区域申请一块内存,让指针指向这个新的空间。
cout << "Person的拷贝构造函数" << endl;
}
//将堆区开辟的数据做释放操作
~Person() {
cout << "Person的析构函数调用" << endl;
if (p_height != NULL) {
delete p_height;
p_height = NULL;
}
}
int p_age;
int* p_height;//弄一个堆区的数据,使用完后让程序员自己释放该数据,利用到析构函数!!!
};
//使用一个已经创建完毕的对象来初始化一个对象
void showing() {
Person p1(18,180);
cout << p1.p_age << " " << endl;//身高怎么打印
Person p2(p1);
cout << p2.p_age<<" "<<endl;//
}
int main() {
showing();
system("pause");
return 0;
}
/*
Person的有参构造函数
18
Person的拷贝构造函数
18
Person的析构函数调用
Person的析构函数调用
*/
- 6)初始化列表
class Person {
public:
/*Person(int a,int b) {
p_a = a;
p_b = b;
}传统初始化,与下面的等效*/
Person(int a, int b) :p_a(a), p_b(b) {
}//初始化列表
int p_a;
int p_b;
};
- 7)类对象作为另外一个类的成员
当类中成员是其他类的对象时,我们称该成员为 对象成员
构造顺序:先调用对象成员的构造,再调用本类构造
析构顺序与构造相反
3.对象模型和thisz指针
-
1)静态成员
- 静态成员变量
- 类内声明,类外初始化
- 所有对象共享同一份数据,所以可以在类外通过 类名::静态成员变量去访问
- 静态成员函数
- 所有对象共享同一个函数,所以可以在类外通过 类名::函数名去访问!!!!!
- 静态成员函数只能访问静态成员变量
//静态成员变量 #include <iostream> using namespace std; //静态成员也有访问权限 class Student { public: static int s_a; private: static int s_b; }; int Student::s_a = 0; int Student::s_b = 0; //静态成员的访问方式 //1.通过对象 void text1() { Student s1; s1.s_a = 100; cout << "s1.s_a= " << s1.s_a << endl; Student s2; s2.s_a = 50; cout << "s1.s_a= " << s1.s_a << endl;//共享同一份数据,数据与s2的相同 cout << "s2.s_a= " << s2.s_a << endl; //无法调用私有静态变量s_b //2.通过类名 cout << "s_a= " << Student::s_a << endl;//数据也是与上面的相同 //Student::s_b私有权限访问不到 } int main() { text1(); } /*s1.s_a= 100 s1.s_a= 50 s2.s_a= 50 s_a= 50 */
- 静态成员变量
-
2)成员变量和成员函数分开存储
- 只有非静态变量才属于类的对象,静态成员函数、成员,非静态函数都不属于类的对象
- 如果类为空,创建一个空对象,它也会占一个字节的空间
- 如果类上面不为空,根据非静态成员变量的类型(int----4字节等)分配空间
-
3)this指针
this指针本质是指针常量,指针的指向不能修改
- this指针指向被调用的成员函数--所属的对象
- this指针是隐含在每一个非静态成员函数内的一种指针,不需要定义,直接使用
- this指向的是调用对象本身,*this表示这个对象本体
class Person{ public: //!!!!!!!!!!!!当形参和成员变量同名时,可以用this指针区分 Person(int age){ this->age=age; } Person& personAdd(Person &p){ this->age+=p.age; return *this; } int age; }; int main(){ Person p1(10); Person p2(10); p2.personAdd(p1).personAdd(p1); cout<<p2.age<<endl; } /*30*/
-
3)空指针访问成员函数
可以创建一个空的指针(Person * p=NULL)
,这个对象p不能通过“this->成员变量”的形式访问成员内部的数据,因为this为空。
-
4)const修饰成员函数
- 常函数:在成员函数后面加const,修饰的是this指针,本身它的指向不能修改,加了之后,它的值也不能修改
- 常对象:在对象前面加const,也是不能修改属性的
- 常对象只能调用常函数
如果一定要修改,成员属性声明时,加一个关键字mutable
4.运算符重载
-
1)对已有运算符进行定义,赋予其另一种功能,以适应不同的数据类型
-
2)加号运算符重载
运算符重载也可以发生 函数重载
eg:上面想完成 Person p4=p1+20;
只需要将上面的函数的参数进行修改
Person operate+(Person &p1,int num ){
//p2也改为num......
}
注意:对于!内置!的数据类型表达式的运算符是不可能被改变的
3)赋值 运算符重载
5.继承
-
1)继承的基本语法与概念
- class 子类名: 继承方式(public、private、protect) 父类名{
};
- 派生类的成员一类是从基类继承过来的(共性),一类是自己增加的成员(个性)
继承的好处:减少重复的代码
-
2)一些专有名词
子类----->派生类
父类----->基类
-
3)继承方式
- 公共继承(public)
- 保护继承(protect)
- 私有继承(private)
-
4)继承的对象模型
- 父类中所有非静态成员属性是都会被子类继承下去的
- 父类中的私有成员属性 是被编译器给隐藏了,因此是访问不到,但是它确实是被继承下去了
-
5)继承中 构造和析构顺序
继承中:先构造父类,再构造子类,析构顺序与构造相反
-
6)继承同名非静态成员处理方式
解决的问题:当子类与父类出现同名的非静态成员时,通过子类对象,访问子类或父类的同名数据
-
访问子类同名对象(属性和函数)
直接访问即可
-
访问父类同名对象(属性和函数)
加作用域::
-
class Father{
Father(){
age=100;
}
void func(){
cout<<"Father_func()调用"<<endl;
}
void func(int a){
cout<<"Father_func(int a)调用"<<endl;
}
int age;
}
class Son:public Father{
Son(){
age=50;
}
void func(){
cout<<"Son_func()调用"<<endl;
}
int age;
}
void test1(){
Son s1;
cout<<"Son_age="<<s1.age<<endl;
cout<<"Father_age="<<s1.Father::age<<endl;
}
void test2(){
Son s2
s2.func();
s2.Father::func();
//如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏所有的同名成员函数,不管你参数是否不同
s2.Father::func(100);
}
int main(){
test1();
}
7)继承同名静态成员处理方式
- 可以同上6)通过子类对象,访问同名的数据
也可以利用类名直接访问(Son::age)(Son::func())(Son::Father::age第一个::代表用类的方式访问,第二个::代表在Father类的作用域下)(Son::Father::func()同前)
6.多态
-
1)多态的概念
-
2)动态多态的满足条件及使用
- 有继承关系
- 子类要重写父类的虚函数(函数前面加一个关键字virtual)
- 动态多态的使用:父类的指针或引用,指向子类对象
- 重写:函数返回类型 函数名 参数列表完全相同
//静态多态与动态多态
#include <iostream>
using namespace std;
class Animal {
public:
//virtual
void yell() {
cout << "动物在叫啊!" << endl;
}
};
class Cat :public Animal {
public:
void yell() {
cout << "小猫在叫啊!" << endl;
}
};
class Dog :public Animal {
public:
void yell() {
cout << "小狗在叫啊!" << endl;
}
};
//父类函数,未加virtual关键字时:地址早绑定,在编辑阶段就确定了函数的地址
//如果要执行具体的动物:猫狗说话,地址就不能提前绑定,需要在运行阶段进行绑定----->地址晚绑定,加virtual关键字
void doYell(Animal &animal) {
animal.yell();
}
void test01() {
Cat cat;
doYell(cat);//相当于:Animal &animal=cat;
Dog dog;
doYell(dog);//相当于:Animal &animal=dog;
//父类的指针指向子类的对象
Animal* animal1 = new Dog;
animal1->yell();
}
int main() {
test01();
}
/*父类函数未加virtual前:
动物在叫啊!
动物在叫啊!
动物在叫啊!
父类函数加了virtual后:
小猫在叫啊!
小狗在叫啊!
小狗在叫啊!
*/
- 3)多态的原理
-
解释:本身这个cat是子类的对象,当你调用animal.speak();时就是去找子类记录表中这个函数的地址,而原本的函数已经被重写,所以调用了重写后的函数
-
4)纯虚函数和抽象类
-
5)虚析构和纯虚析构