核心编程
欢迎来到面向对象的开始篇
内存的分区模型
— 再程序执行的时候系统会自动划分四个区域
代码区:存放函数体的二进制代码,由操作系统进行管理的
全局区:存放全局变量和静态变量以及常量
栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
堆区:由程序员分配和释放,若程序员不释放程序结束时由操作系统回收
@ 代码区
代码区放的都是一些二进制的代码0101
— 在程序运行前分的一个区域
在还没有运行exe程序之前代码区就已经有了
— 存放的是CPU的执行的机器指令
— 代码区的特点
—* 共享
对于平频繁被执行的程序,只需要在内存里有一份代码就行
程序执行多次exe,不需要放两端代码
只要生成成功了它就只有一段代码
—* 只读
防止程序被意外篡改
全局区
— 全局区负责
全局变量
静态变量
—* 常量
字符串常量
—** 其他常量
就是自己const修饰的变量
— 该区域的数据在程序结束之后由操作系统管理释放
— 全局变量
没有写在函数体内的变量比如main函数的上面namespace的下面
全局变量与局部变量不在同一个内存区中
假设局部变量的内存的地址是161开头(被强制转换后的结果)
全局变量的内存的地址是103开头(这也是被强制转换后的结果)
—被static修饰前缀的就是静态变量
— 全局变量与静态变量还有字符串常量离得很近基本都是175开头的
— const修饰变量
—* const修饰的全局变量
2858开头的内存地址(被强转成int之后的结果)
—* const修饰的局部变量
局部常量与局部变量都是5961开头的
—* 只要是局部区域的他们都不在全局区
栈区
— 程序运行后的区域
— 由编译器自动分配释放存放函数的参数值,局部变量等
—*注意事项:不要返回局部变量的地址,栈区开辟的数据会由编译器自动释放
int* zhanVqu()
{
int a=10;//局部变量存放在栈区, 栈区的数据在函数执行完后自动释放
return &a;//返回局部变量的地址
}
int main()
{
int *p
cout<<*p<<endl;//第一次编译器会做一次保留
cout<<*p<<endl;//第二次不再被保留
}
返回输出自多保留一次。因为在栈区会自动被释放
堆区
— 由程序员分配释放,如果不亲自释放会一直留在内存里直到程序被关闭由系统来做回收
— 如果要在堆区新建数据就要使用new关键字
利用new可以把数据创建在堆区
int* niunew{
int *shuVjv = new int(1);
return shuVjv;
}
int main()
{
int *p=niunew();
cout<<*p<<endl;
cout<<*p<<endl;
return 0;
}
niunew里的shuVjv其实还只是局部变量还是放在栈上的
指针的本质其实也是一个局部变量放在栈上
指针保存的数据是放在了堆区
但是指针的地址是放在堆区的
只是堆区的数据地址编号用栈上的指针保存住了
当解引用的时候也是拿到他解出来的堆区的数值1了
new运算符
— new操作符是在堆区开辟数据
— 动态new出来的对象都是没有名字的,只能使用指针
int* a =new int(11);
*a = 12;
— 在堆区开辟的数据需要用delete关键字自行释放
int* niunew()
{
int *shuVjv = new int(10);//返回一个该数据类型的指针new一个int那就是int不能用doblue类型来接收
return shuVjv;
}
viod shuVchu()
{
int *p=niou=niunew();
cout<<*p<<endl;
}
int main()
{
shuVchu();
}
如果不手动释放就永远都不会释放
shuVchu()
{
int *p =niunew();
delete p;
cout<<*p<<endl;
}
int main()
{
shuVchu();
return 0;
}
因为p被释放掉了,再次访问就是非法的了。
new数组
new数组的创建
void shuVzunew()
{
int *shuVzu= new int[10];//10代表数组中的十个元素
delete[] shuVzunew;//释放数组的时候必须在delet胖必须要加一个中口号
}
引用
引用就是给一个变量起一个别名
引用的基础语法
— 定义的语法
数据类型 &别名 =原名;
int a=10;
int&b=a;
通过更改b中的值也就直接更改了a里面的值
引用的注意事项
— 引用必须初始化
初始化后就不能更改他的指向了
等于号只能说赋值
引用做函数参数
— 引用函参数是可以通过形参更改数值的
— 在普通的函数里想让形参改变实参的话就需要用地址传递
— 引用函数的优点可以简化指针
viod yinVyongVhanVshu(int &a,int &b)
{
int beiVzi =a;
a=b;
b=a;
}
int main()
{
int a=9;
int b=10;
yinVyongVhanVshu(a,b);
cout<<a<<b<<endl;
}
引用做函数的返回值
— 引用可以作为函数的返回值存在
— 不要返回局部变量的引用
因为局部变量的地址存放在栈区
用完之后就会被释放掉
只能被输出一次这是编译器采取的保留一次的措施
— 函数的调用可以作为左值
int& zuoVzhi()
{
//static的作用就是把变量创建在全局区,由软件被关闭的时候系统会自动释放
static int a = 100;
return a;
}
int main()
{
int b=zuoVzhi();
cout<<b<<endl;
cout<<b<<endl;
zuoVzhi()=1000;
}
引用的函数需要在int旁边不加空格紧挨着一个&
zuoVzhi()=1000这句代码调用完之后返回的是一个调用
调用只要一方有修改另一方就跟着有修改
zuoVzhi函数里的最后的return谁在用左的调用它那么通过return传回去给那个变量
引用的本质
引用的本质在C++中内部实现是一个指针常量
指针常量是指针的指向不可以修改的但是值是可以改动的
常量引用
— 用来修饰形参防止误操作
— 引用无法与常量挂钩
int & a =10;
— 使用常量const修饰符可以
const int & b =10;
加上const修饰符之后编译器将代码修改成一个临时的变量来等于10,然后再用const int &b=哪一个临时的变量
加上const修饰符之后值将无法修改
—* 用引用来做形参
在里面被引用的变量的值任何改动都会改变
加上const之后就只能被读取不能写入新的
高级函数
函数的默认参数
— C++中函数的形参列表的形参可以有默认值的
int hanVshuVmoVrenVzhi(int a,int b=20,int c=30)
{
return a+b+c;
}
int main()
{
cout<<hanVshuVmoVrenVzhi(10)<<endl;
return 0;
}
hanVshuVmoVrenVzhi的函数里bc都有初始化,如果调用输入值编译器会默认把值给a
如果已经有默认值了这是可以更改的
如果在某个形参变量有了默认值那么就从那个形参变量的右边的所有变量都要有默认值
函数创建可以有默认值,函数声明不可以有任何默认值
int hanVshuVmoVrenVzhi(int a,int b,int c);
int main()
{
cout<<hanVshuVmoVrenVzhi(10)<<endl;
return 0;
}
int hanVshuVmoVrenVzhi(int a,int b=20,int c=30)
{
return a+b+c;
}
函数的占位参数
C++函数的形参列表里可以有占位参数,用来占位,调用函数时必须填补该位置
— 用法
void hanVshuVzhanVweiVfu(int a,int)
{
}
VV:
这个函数体中的int没有名字只有数据类型的int就是占位符
VV./
占位参数还可以有默认参数
VV:
void hanVshuVzhanVweiVfu(int a,int = 10)
{
}
函数重载
函数名可以相同
— 函数重载满足条件
同一个作用域下
函数名相同
函数参数类型不同或者个数不同或者顺序不同
函数的返回值不可以作为函数重载的条件
void chongVzai()
{
cout<<"chongVzai"<<endl;
}
void chongVzai(a)
{
cont<<"chongVzai(a)"<<endl;
}
void chongV(doblue a)
{
cout<<"chongVzai(double a)"<<endl;
}
void(int a,double b)
{
cout<<"chongVzai(int a,double b)"<<endl;
}
返回值不能作为函数重载的条件
void(int a,double b)
{
cout<<"chongVzai(int a,double b)"<<endl;
}
int (int a,double b)
{
cout<<"chongVzai(int a,double b)"<<endl;
}
两个重载都一样只有只有返回类型不一样是不允许的
函数重载的注意事项
— 引用作为重载的条件
void hscz(int &a)
{
cout<<"hscz(int &a)"<<endl;
}
void hscz(const int &a)
{
cout<<"hscz(const int&a)"<<endl;
}
int main()
{
a=10;
hscz(a);
hscz(10);
}
^VV:
调用变量如果是变量那么就会匹配到没有const的里面
调用变量如果是数字常量就会匹配到有const的里面
—*引用必须要有一个合法的内存空间
—*如果走下面的const就会临时创建一个临时的变量:
const int &a=10;
VV/;
— 函数重载碰到函数默认参数
函数重载碰到就会出现二义性
void hscz(int a)
{
cout<<"a"<<endl;
}
void hscz(int a,int b)
{
cout<<"ab"<<endl;
}
int main()
{
hscz(10);//错
hscz(10,20);//对
}
^VV:
因为参数的类型相同就不知所措要调用哪一个
第二个对是因为二义性的原因,所以就调用了有两个参数的
VV/;
类和对象
C++面向对象的三大件:封装、继承、多态
C++万物皆可对象
^VV:
人:
姓名,年龄,体重,吃,喝,拉,撒,睡,玩乐,生病,健康等等
地球:
天空,陆地,海洋,动物,植物,森林,沙漠,沼泽,农村,城市
学校:
老师,校长,食堂阿姨,男同学,女同学,教室,办公室,医务室,操场
VV/;
封装
— 将属性与行为作为一个整体,表现生活中的事务
— 将属性与行为加以权限限制
— 封装意义
在设计类的时候,属性和行为写在一起,表现事物
语法
class 类名
{
访问权限:
属性/行为
}
代码实例:
//定时类
class dingVshi{
//访问权限
public:
//属性就是变量
//小时
int xiaoVshi =0;
//分钟
int fenVzhong = 0;
//秒
int miao=0;
//行为就是函数
//定时钟
dingVshiVjian()
{
cout<<xiaoVshi<<"时";
cout<<fenVzhong<<"分钟";
cout<<miao<<"秒"<<endl;
}
int main()
{
//对象变量创建
dingVshi a;
cin>>a.xiaoVshi;
cin>>a.fenVzhong;
cin>>a.miao;
a.dingVshiVjian();
}
}
class是创建类的关键字
访问权限关键字
— 三种访问权限
public 公共权限 成员类内外都可以访问到
protected 保护权限 只有类内使用
private 私有权向 只有类内使用
class fangVwenVquan
{
public:
int xueVxiao;
protected:
int riVji;
private:
int ziVjiVjia;
}
struct与class的区别
— struct的默认权限是公共的public
— class的默认权限是私有的private除了作用于类外就不能使用了
将成员属性设为私有
— 将成员属性设置为私有,可以自己控制读写权限
— 对于写的权限,可以检查数据的有效性
class syqx//私有权限
{
public:
//public区进行间接的访问和改写
void setmingVzi(string m_z)
{
mingVzi;=m_z
}
void getmingVzi()
{
return m_z;
}
void getduVquVnianVling()
{
//之前没有赋值可以再这里赋值
nianVling=18;
return nianVling;
}
void setfenVshu(int f_s)
{
if(f_s<0||f_s>100)
{
fenVshu=0;
cout<<"你作弊了"<<endl;
return;
}
fenVshu=f_s;
}
void geth_q_f_s()//获取分数
{
rreturn fenVshu;
}
private:
//不能被直接访问的区域
string mingVzi//写与读
int nianVling//只读
//可以有限制的条件
int fenVshu//可以限制的
}
int main()
{
syqx a;
a.setmingVzi("张三");
cout<<"姓名:"<<a.getmingVzi();<<endl;
cout<<"年龄:"<<a.getduVquVnianVling();<<endl;
cout<<"分数:"<<a.geth_q_f_s()<<endl;
}
属性设为私有会对外的在public的属性变量来间接访问或修改
set是设置
get是获取
set与get这些都不是什么关键字这只是命名规则共识
set与get只是共识get也是能修改数值的,get不叫get叫别的也行(这里我说的是在类内里的方法里面不是类的外面)
对象的初始化与清理
— 在现实生活中的电子产品都会有出厂设置,如果没有出厂设置如果发生问题都是未知的。软件在手机里有一天不用了,就要把软件给清掉确保信息数据的安全
— C++中的面向对象来源于生活,每个对象也都会有初始设置以及对象销毁前的清理数据的设置。利用构造函数和析构函数即可
构造函数和析构函数
— 构造函数就是初始化
— 析构函数就是清理
— 构造和析构函数都是由编译器自动调用完成工作
— 如果自己不提供构造和析构函数系统就会自动创建
— 系统提供的都是空的实现
— 析构函数主要作用于创建对象的时候为对象的成员属性赋值,系统会自动调用不必手动调用
— 析构函数在对象销毁前系统自动调用,执行清理工作
— 构造函数语法
类名()
{
}
析构函数没有返回值也不用写void
函数名称与类名相同
构造函数可以有参数,因此可以发生重载
程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
只要创建对象了,他就会自动调用构造函数不需要手动调用
— 析构函数
~类名()
{
}
析构函数没有返回值与void
函数名称与类名是相同的,要在名称钱加一个波浪号~
析构函数不可以有参数,因此不嫩该发生重载
析构函数会在对象销毁前自动调用,无需人为手动,只会调用一次
对象的初始化与清理实例
— 构造函数的实例
class ren
{
public:
ren()//这是一个没有参数的版本//与类同名
{
cout<<"构造函数调用"<<endl;
//要想在函数外调用它就要创建在pulic作用域里面
//如果没有创建系统也会自动创建不过什么都没有
}
void ceVshi()
{
//创建对象
ren a;//创建及被调用要在调用ceVshi函数的时候这个ren a就会被调用
}
int main()
{
ceVshi();//调用ceVshi的同时也会自动调用ren a
return 0;
}
}
— 析构函数的实例
class ren
{
public:
~ren()//不需要返回值和void//与类名相同//有波浪符~//不能有参数//不能发生重载
{
cout<<"析构函数的调用"<<endl;
//在对象销毁前自动调用一次
}
void ceVshi()
{
ren a;
}
int main()
{
//什么都没有但是会自动调用析构函数~ren
//如果在main函数里面调用只有在出现按住任意键的一刹那才会出现0.1秒
ren a;
return 0;
}
}
构造函数的分类及调用
— 两种分类方式
按参数分为:有产构造与无惨构造
按类型分类:普通构造与拷贝构造
— 三种调用方式
括号法
显示法
隐式转换法
— 实例
class ren
{
ren()//无惨构造函数(默认构造函数)
{
cout<<"ren的构造函数调用"<<endl;
}
ren(int a)//有参构造函数
{
nianVling=a;
cout<<"ren的构造函数调用"<<endl;
}
~ren()
{
cout<<"ren的析构函数调用<<endl;
}
//拷贝构造函数
/*
复制一份i
在传进来的时候要加入一个const用于防止被修改
与此同时用引用的方式传进来&
因为引用会同时修改i中的参数所以要加入const来赋值的过程中不修改i中的值
*/
ren(const ren &i)//p3
{
nianVling=p.nianVling;
//再用拷贝构造的时候把用p3把p2传进去赋值给nianVling变量
}
int nianVling;
}
— 调用的实例
ren p1;//默认构造函数的调用
ren p2(10)//有参构造函数的调用
ren p3(p2);//拷贝构造函数的调用
调用默认构造函数的时候不要加小括号,如果加了就会被认为是声明
显示法的调用
ren p4 = ren(10);//显示的有参构造把10传进去
ren p5 = ren(p4);显示的拷贝构造
如果把右面的单独拿出来就叫做匿名对象
ren(10);//当前的这一行执行结束就会被立即回收匿名对象
不能使用拷贝函数初始化匿名对象,因为这样会被判定重定义
隐式转换法的调用
ren p6=10;//这个就相当于 ren p6= ren(10);
拷贝构造函数调用的时机
— C++中拷贝构造函数调用时机通常有三种情况
使用一个已经创建完毕的对象来初始化一个新对象
viod ceVshi()
{
ren p1(10);
ren p2(p1);//拷贝构造函数调用
}
值传递的方式给函数参数传值
viod gongVzuo(ren p)//会拷贝一个独立新的数据因为没有&引用符号,这也是一个拷贝只不过是独立的
{
}
viod ceVshi2()
{
ren p;//调用默认构造函数
gongVzuo(p);//调用拷贝函数
}
以值方式返回局部对象
ren gongVzuo1()
{
ren p1;
return pi;
}
viod ceVshi3()
{
ren p=gongVzuo2();
}
代码段
class ren
{
public://让类外可以访问
ren()
{
cout<<"ren的默认结构函数的调用"<<endl;
}
ren(int N_L)
{
cout<<"有参构造函数的调用“<<endl;
nianVling=N_L;
}
~ren()
{
cout<<"默认析构函数的调用"<<endl;
}
int nianVling;//类的变量最好在函数里进行赋值比如ren(int N_L)函数体力的语句
ren(const ren & p)
{
cout<<"拷贝构造函数的调用<<endl;
nianVling=p.nianVLing;
}
}
viod ceVshi()
{
ren p1(10);
ren p2(p1);//拷贝构造函数调用
}
viod gongVzuo(ren p)//会拷贝一个独立新的数据因为没有&引用符号,这也是一个拷贝只不过是独立的
{
}
viod ceVshi2()
{
ren p;//调用默认构造函数
gongVzuo(p);//调用拷贝函数
}
ren gongVzuo1()
{
ren p1;
return pi;
}
viod ceVshi3()
{
ren p=gongVzuo2();
/*gongVzuo函数中有一个ren的变量p1gongVzuo函数又是一个有着ren类型返回值的类型,就正好吧那个p1返回给这个ceVshi3中的p这个变量*/
}
int main()
{
ceVshi1();
ceVshi2();
ceVshi3();
}
构造函数调用规则
— 编译器会默认给初始化三个函数
默认构造函数(无参,函数体为空)
默认析构函数(无参,函数体为空)
默认拷贝构造函数,对属性进行值拷贝
— 构造调用规则
如果用户定义有参构造函数,c ++不在提供默认无参构造,但是会提供默认拷贝构造
如果用户定议拷贝构造函数,C ++不会再提供其他构造函数
— 代码演示
class ren
{
public:
ren()//默认构造函数
{
cout<<"ren的默认构造函数调用"<<endl;
}
ren(int N_L)//有参构造函数
{
nianVling=N_L;
cout<<"ren的有参构造函数调用"<<endl;
}
ren(const ren&p)//拷贝函数
{
nianVling=p.nianVling;
cout<<"ren的拷贝默认构造函数"<<endl;
}
~ren()//默认析构函数
{
cout<<"ren的默认析构函数调用"<<endl;
}
int nianVling;//年龄,在构造函数里赋值
}
void ceVshi01()//默认构造函数的赋值调用
{
ren p;
p.nianVling=18;
ren p2(p);//p2执行的是拷贝构造函数ren(const ren&p)
}
int main()
{
ceVshi01();
}
。 如果没有提供默认构造,创建了一个ren a这个变量就会报错,因为创建及被调用
。 如果写了有参构造函数,编译器是不会自动提供生成一个默认构造函数的,需要手动创建
。 但是拷贝构造函数还是给自动提供生成的
深拷贝与浅拷贝
。 浅拷贝就是编译器提供的拷贝构造函数做的操作叫简单的赋值操作。编译器提供的拷贝内容都是浅拷贝
。 深拷贝就是堆区重新申请空间,进行拷贝的操作
class ren
{
public:
ren()
{
cout<<"ren的默认构造函数调用"<<endl;
}
ren(int N_L,int S_G)//年龄、身高
{
nianVling=N_L
new int(S_G);//就相当于shenVgao = new int(S_G)
cout<<"有参构造函数调用"<<endl;
}
int nianVling;
int *shenVgao;
~ren()
{
if(shenVgao != NULL)
{//判断shenVgao的变量是否为空
delete shenVgao;//堆区变量要在析构函数中使用delete关键字进行释放
shenVgao=NULL;//防止变成野指针的语句
}
cout<<"析构函数调用"<<endl;
}
ren(const &p)//手动创建的深指针
{
cout<<"ren的拷贝构造函数"<<endl;
shenVgao=p.shenVgao;
shenVgao=new int(p.shenVgao);
}
}
void ceVshi()//如果没有创建自己的拷贝构造函数在释放堆区变量的时候发生错误的代码
{
ren p1(20,180);//左边的是nianVling右边是shenVgao的变量调用的是 ren(int N_L,int S_G)的构造函数
cout<<"p1的年龄为:"<<p1.nianling<<"p1的身高为"<<p1.shenVgao<<endl;//注意这里指针部分要使用*用作解引用否者输出的是地址
ren p2(p1)/*//这个语句要注意是从外部传进来到 ren(int N_L,intS_G)的指针匹配到这块语句了 ,因为这里有一个new这个表示创建在堆区
//使用new语句是把变量创建在堆区,需要手动释放
//在p2拷贝p1的时候p2的地址也复制了P1的地址所以在释放的时候会发生异常发生了非法操作就是说重复释放了两次地址
//编译器自动生成的拷贝析构函数是浅拷贝
//解决这个问题的方法就是自己创建一个拷贝析构函数,也就是深拷贝
*/
cout<<"p2的年龄为:"<<p2.nianling<<"p1的身高为"<<p2.shenVgao<<endl;//注意这里指针部分要使用*用作解引用否者输出的是地址
}
int main()
{
system("pause")
return 0;
}
编译器给生成的浅拷贝如果创建的时候使用new创建在堆区就会有问题
初始化列表
代码:
class ren
{
ren():a(10),b(20),c(30)
{//flag01
}
public:
int a;
int b;
int c;
}
void ceVshi()
{
ren p;
}
int main()
{
system("pause");
return 0;
}
注解:
flag01的解释
ren():a(10),b(20),c(30)
{
}
{
这里的代码如果在外面创建了构造变量,那么ren p对象内的三个参数就会变成括号里的
比如用ceVshi中的代码什么都没做只是创建了一个ren的自定义的变量,直接赋值了参数
}
新方法:
//flag01
ren p(int int A,int B,int C):a(A),b(B),c(C)
{
}
void ceVshi2
{
ren p(1,2,3);
}
{
这样可以灵活的在创建ren变量的时候通过自己给如的参数赋值不用再向刚才的那种很死板的方法来赋值了
可以直接在括号里使用逗号来隔开每个参数不用再用p.A p.b之类的了
}