目录
本阶段主要针对C++面向对象编程技术做详细讲解,探讨C++中的核心和精髓。
1 程序的内存模型
C++在执行代码时,将内存分为4个区域,不同区域存放的数据,赋予不同的生命周期。
- 代码区:存放函数体的二进制代码,由操作系统管理;
- 全局区:存放全局变量、静态变量和常量;
- 栈区:由编译器自动分配释放,存放函数的参数值、局部变量等;
- 堆区:由程序员分配和释放,若程序员不释放,则程序结束时由操作系统回收。
1.1 内存四区---代码区
代码区是程序运行前分的,在程序编译后运行前,分为代码区和全局区。代码区存放CPU执行的机器指令。
特点:
- 共享:对于频繁被执行的程序,只需要在内存中有一份代码即可。
- 只读:防止程序域外修改其中的指令。
1.2 内存四区---全局区
存放全局变量、静态变量和常量。该区域的数据在程序结束后由操作系统释放。
代码如下:
#include <iostream>
using namespace std;
//定义全局变量
int g_a=10;
int g_b=10;
const int c_g_a=0;
const int c_g_b=0;
int main()
{
//全局区
//存放全局变量
//定义普通局部变量
int a=10;
int b=10;
cout<<"局部变量a的地址为:"<<(long long)&a<<endl;
cout<<"局部变量b的地址为:"<<(long long)&b<<endl;
cout<<"全局变量g_a的地址为:"<<(long long)&g_a<<endl;
cout<<"全局变量g_b的地址为:"<<(long long)&g_b<<endl;
//定义静态变量
static int s_a=0;
static int s_b=0;
cout<<"静态变量s_a的地址为:"<<(long long)&s_a<<endl;
cout<<"静态变量s_b的地址为:"<<(long long)&s_b<<endl;
//定义常量
//字符串常量
cout<<"字符串常量的地址为:"<<(long long)&"hello C++"<<endl;
//const修饰的全局变量
cout<<"全局常量c_g_a的地址为:"<<(long long)&c_g_a<<endl;
cout<<"全局常量c_g_b的地址为:"<<(long long)&c_g_b<<endl;
//const修饰的局部变量
const int c_l_a=0;
const int c_l_b=0;
cout<<"局部常量c_l_a的地址为:"<<(long long)&c_l_a<<endl;
cout<<"局部常量c_l_b的地址为:"<<(long long)&c_l_b<<endl;
return 0;
}
输出如下:全局变量的内存地址相近,与局部变量的地址相差明显远。
1.3 内存四区---栈区
由编译器自动分配释放,存放函数的参数值、局部变量等,不要返回局部变量的地址(函数执行完之后会自动释放),栈区开辟的数据由编译器自动释放。
代码如下:
#include <iostream>
using namespace std;
int* func()
{
int a=0;//局部变量
return &a;//返回局部变量的地址 报错(函数返回类型加一个 * 返回局部变量的地址就不会报错)
}
int main()
{
//接收func函数的返回值
int * p=func();
cout <<*p<<endl;//报错
return 0;
}
错误示例:
1.4 内存四区---堆区
由程序员分配和释放,若程序员不释放,则程序结束时由操作系统回收。在C++中用new在堆区开辟内存。
代码如下:
#include <iostream>
using namespace std;
int* func()
{
//利用new将数据开辟到堆区
int * p=new int(10);//用指针接收内存地址编号
return p;
}
void func1()
{
//在堆区开辟数组
int * arr=new int[10];//用指针接收内存地址编号
for(int i=0;i<10;i++)
{
arr[i]=i+100;
}
for(int i=0;i<10;i++)
{
cout<<arr[i]<<endl;
}
//释放数组
delete[] arr;
}
int main()
{
//接收func函数的返回值,接收的是在堆中开辟的地址,而不是局部变量在栈中的地址
int * p=func();
cout <<*p<<endl;
cout <<*p<<endl;
//手动释放内存
delete p;
cout<<*p<<endl;//报错 内存已被释放,再访问不会输出正确的值
func1();
return 0;
}
输出如下:
2 C++中的引用
引用的作用:给变量取别名。(别名和原名操作同一个内存地址)
语法:数据类型 &别名=原名;
2.1 引用的基本语法
语法:数据类型 &别名=原名;
PS:引用必须初始化,初始化后不可改变。
代码如下:
#include <iostream>
using namespace std;
int main()
{
//引用
int a=9;
//创建引用
int &b=a;
//int &c;//报错,必须初始化
cout<<"a="<<a<<endl;
cout<<"b="<<b<<endl;
b=100;
cout<<"a="<<a<<endl;
cout<<"b="<<b<<endl;
int c=20;
//引用被初始化后不可更改,下面的语句实际上是赋值语句
b=c;
cout<<"a="<<a<<endl;
cout<<"b="<<b<<endl;
cout<<"c="<<c<<endl;
return 0;
}
输出如下:
错误示例:
2.2 引用与函数
函数传参时,可以利用引用的技术让形参修饰实参,可以简化指针修改实参。
引用传递和地址传递效果相同。
如果函数的返回值是引用,则函数的调用可以作为左值。
代码如下:
#include <iostream>
using namespace std;
//交换函数
//值传递
void func1(int a,int b)
{
int temp=a;
a=b;
b=temp;
}
//地址传递
void func2(int * a,int * b)
{
int temp=*a;
*a=*b;
*b=temp;
}
//引用传递
void func3(int &a,int &b)
{
int temp=a;
a=b;
b=temp;
}
int main()
{
//引用做函数参数
int a=9;
int b=0;
cout<<"参数传递前"<<endl;
cout<<"a="<<a<<endl;
cout<<"b="<<b<<endl;
//值传递,形参不会修饰实参
func1(a,b);
cout<<"值传递后"<<endl;
cout<<"a="<<a<<endl;
cout<<"b="<<b<<endl;
//地址传递,形参会修饰(改变)实参
func2(&a,&b);
cout<<"地址传递后"<<endl;
cout<<"a="<<a<<endl;
cout<<"b="<<b<<endl;
//引用传递,形参也会修饰实参
func3(a,b);
cout<<"引用传递后"<<endl;
cout<<"a="<<a<<endl;
cout<<"b="<<b<<endl;
return 0;
}
输出如下:
代码如下:
#include <iostream>
using namespace std;
// int& func1()
// {
// int a=9;//局部变量,存放在栈区
// return a;
// }
int& func2()
{
static int a=9;//静态变量,存放在全局区
return a;
}
int main()
{
//引用做函数返回值,不要返回局部变量的引用
//函数的调用可以作为左值
int a=9;
int b=0;
// int& ref=func1();
// cout<<"ref="<<ref<<endl;
int& ref2=func2();
cout<<"ref2="<<ref2<<endl;
//如果函数的返回值是引用,函数的调用可以作为左值
func2()=1000;
cout<<"ref2="<<ref2<<endl;
return 0;
}
输出如下:
错误示例:返回局部变量的引用
2.3 引用的本质
引用的本质是一个指针常量(指针指向不可更改,指向的数据可以改变)
下面两句代码的效果是一样的。
int& ref=a;
int * const ref=&a;
2.4 常量引用
作用:用于修饰形参,防止误操作
代码如下:
#include <iostream>
using namespace std;
void show(const int& val)
{
//val=99;//报错 常量不可修改
cout<<"val="<<val<<endl;
}
int main()
{
//常量引用:修饰形参
int a=9;
//int& ref=9;//报错 必须引用一块合法的内存空间
//const int& ref=9;//不再报错,编译器会操作为int temp=9; const int& ref=temp;
show(a);
return 0;
}
错误示例:int& ref=9;
错误示例:修改常量
3 函数进阶
3.1 函数的默认参数
C++中,函数的形参是可以有默认值的。
语法:返回值类型 函数名(参数=默认值){}
PS:如果某个形参有了默认值,则该形参及其后面的形参都必须有默认值。
如果函数声明有默认参数,则函数实现不能有默认参数。
代码如下:
#include <iostream>
using namespace std;
//如果函数声明有默认参数,则函数实现不能有默认参数。
//int func(int a,int b=20,int c=30);
// 函数默认参数
int func(int a,int b=20,int c=30)
{
return a+b+c;
}
int main()
{
//cout<<func(10,20,30)<<endl;
cout<<func(10)<<endl;
//如果自己传入了数据,则函数用传入的数据运行
cout<<func(10,30)<<endl;
return 0;
}
输出如下:
错误示例:函数声明有默认参数,函数实现也有默认参数。
3.2 函数的占位参数(用法未知)
C++中函数的形参列表里可以有占位参数,用来占位,调用函数时必须填补该位置。
语法:返回值类型 函数名(数据类型){}
PS:占位参数也可以有默认值,如int func(int a,int =10)
代码如下:
#include <iostream>
using namespace std;
// 第二个int为占位参数
int func(int a,int)
{
cout<<"this is func"<<endl;
}
int main()
{
func(10,10);
return 0;
}
错误示例:调用时不传占位参数.
3.3 函数重载
作用:函数名可以相同,提高复用性。
函数重载满足条件:
- 同一作用域下(目前写的都是全局函数,都在全局作用域下)
- 函数名称相同
- 函数参数类型不同 或 个数不同 或顺序不同
PS:函数的返回值不可作为函数重载的条件。
代码如下:
#include <iostream>
using namespace std;
// 函数重载
void func()
{
cout<<"this is func"<<endl;
}
//函数名相同
void func(int a)
{
cout<<"this is func(int a)"<<endl;
}
//参数类型不同
void func(double a)
{
cout<<"this is func(double a)"<<endl;
}
//参数个数不同
void func(int a,double b)
{
cout<<"this is func(int a,double b)"<<endl;
}
//参数顺序不同
void func(double a,int b)
{
cout<<"this is func(double a,int b)"<<endl;
}
int main()
{
func();
func(10);
func(3.14);
func(10,3.14);
func(3.14,10);
return 0;
}
输出如下:
PS:引用作为重载条件
函数重载碰到默认参数。
代码如下:
#include <iostream>
using namespace std;
// 函数重载:引用作为重载条件
void func(int &a )
{
cout<<"this is func(int& a )"<<endl;
}
void func(const int &a )
{
cout<<"this is func(const int &a )"<<endl;
}
//函数重载时定义了默认参数,会存在二义性
void func2(int a,int b=10)
{
cout<<"this is func2(int a,int b=10)"<<endl;
}
void func2(int a )
{
cout<<"this is func2(int a )"<<endl;
}
int main()
{
int a=10;
func(a);
func(10);
//func2(10);//报错 存在两个相同的重载函数
func2(10,20);
return 0;
}
输出如下:
错误示例:函数重载时定义默认参数。
4 类和对象
C++面向对象的三大特性:封装、继承、多态
具有相同性质的对象,抽象为类
4.1 封装
封装的意义:将属性和行为作为一个整体,表现生活中的事物,并将属性和行为加以权限控制。
4.1.1 类的定义及实例化对象
语法:class 类名{访问权限:属性/行为};
类中的属性和行为统称为成员,属性也可以称为成员属性或成员变量,行为也可以称为成员函数或成员方法。
代码如下:
#include <iostream>
using namespace std;
//设计一个圆类,求圆的周长
const double PI=3.14;
//class代表设计一个类,类后面紧跟的是类名称
class Circle
{
//访问权限
public://公共权限
//属性
//半径
int r;
//行为:获取圆的周长
double calculateZC()
{
return 2*PI*r;
}
};
int main()
{
//实例化:通过圆类,创建具体的圆(对象)
Circle c1;
//给圆对象的属性进行赋值
c1.r=10;
cout<<"圆的周长为:"<<c1.calculateZC()<<endl;
return 0;
}
输出如下:
4.1.2 访问权限
访问权限分为三种:
- 公共权限:类内外都可以访问成员
- 保护权限:类内可以访问成员,类外不能访问
- 私有权限:类内可以访问成员,类外不能访问
代码如下:
#include <iostream>
using namespace std;
class Person
{
//公共权限
public:
string name;
//保护权限
protected:
string car;
//私有权限
private:
int passwd;
public:
void func()
{
name="ddd";
car="bench";
passwd=4444;
}
};
int main()
{
//实例化一个对象
Person p1;
//给圆对象的属性进行赋值
p1.name="dddd";
//p1.car="paosij";
//p1.passwd=1234;
return 0;
}
错误示例:修改保护权限和私钥权限的属性。
成员属性设置为私有,可以自己控制读写权限,对于写权限,可以检测数据的有效性。
代码如下:
#include <iostream>
using namespace std;
class Person
{
public:
//设置姓名
void setName(string name)
{
m_name=name;
}
//获取姓名
string getName()
{
return m_name;
}
//获取年龄
int getAge()
{
return m_age;
}
//设置偶像
void setIdol(string idol)
{
m_idol=idol;
}
private:
string m_name;//姓名 可读可写
int m_age=18;//年龄 只读
string m_idol;//偶像 只写
};
int main()
{
//实例化一个对象
Person p;
//设置姓名
p.setName("ddd");
//获取姓名
cout<<"姓名:"<<p.getName()<<endl;
//设置年龄,不存在该函数
//p.setAge("ddd");//报错 不存在该成员setAge
//p.m_age=23;//报错 无法访问
//获取年龄
cout<<"年龄:"<<p.getAge()<<endl;
//设置偶像
p.setIdol("huge");
//获取偶像,没有获取函数
//cout<<"偶像:"<<p.getIdol()<<endl;//报错 不存在该成员getIdol
//cout<<"偶像:"<<p.m_idol()<<endl;//报错 无法访问
return 0;
}
错误示例:
4.1.3 struct 与 class
- struct默认权限为公共
- class默认权限为私有
错误示例:访问class中默认权限下的成员。
4.1.4 案例1:立方体类
题目:设计立方体类,求出立方体的面积和体积,分别用全局函数和成员函数判断两个立方体是否相等。
代码如下:
#include <iostream>
using namespace std;
class Cube
{
private:
//属性:长宽高
int l;
int w;
int h;
public:
//设置\获取属性
void setLong(int ll)
{
l=ll;
}
int getLong()
{
return l;
}
void setWidth(int ww)
{
w=ww;
}
int getWidth()
{
return w;
}
void setHeight(int hh)
{
h=hh;
}
int getHeight()
{
return h;
}
//方法:计算面积、体积、判断两个立方体是否相等
int getSquare()
{
return 2*(l*h+h*w+h*w);
}
int getVolume()
{
return l*w*h;
}
//利用成员函数判断两个立方体是否相等
void isEqual(Cube &c)
{
if(l==c.getLong() && w==c.getWidth() && h==c.getHeight())
{
cout<<"两个立方体相同"<<endl;
}
else
{
cout<<"两个立方体不同"<<endl;
}
}
};
//利用全局函数判断两个立方体是否相等
void isSame(Cube &c1,Cube &c2)
{
if(c1.getLong()==c2.getLong() && c1.getWidth()==c2.getWidth() && c1.getHeight()==c2.getHeight())
{
cout<<"两个立方体相同"<<endl;
}
else
{
cout<<"两个立方体不同"<<endl;
}
}
int main()
{
//创建对象
Cube c1;
c1.setLong(1);
c1.setWidth(2);
c1.setHeight(3);
cout<<"c1的面积为:"<<c1.getSquare()<<endl;
cout<<"c1的体积为:"<<c1.getVolume()<<endl;
Cube c2;
c2.setLong(1);
c2.setWidth(2);
c2.setHeight(3);
//判断两个立方体是否相等
isSame(c1,c2);
c1.isEqual(c2);
c2.setLong(2);
isSame(c1,c2);
c1.isEqual(c2);
return 0;
}
输出如下:
4.1.5 案例2:点和圆(分文件)
题目:设计一个圆形类和一个点类,计算点和圆的关系。
代码如下:
#include <iostream>
using namespace std;
//点类
class Point
{
private:
int p_x;
int p_y;
public:
//设置/获取X,Y
void setX(int x)
{
p_x=x;
}
int getX()
{
return p_x;
}
void setY(int y)
{
p_y=y;
}
int getY()
{
return p_y;
}
};
//圆类
class Circle
{
private:
int c_r;//半径
Point center;//圆心
public:
//设置/获取半径圆心
void setR(int r)
{
c_r=r;
}
int getR()
{
return c_r;
}
void setCer(Point cer)
{
center=cer;
}
Point getCer()
{
return center;
}
};
//判断点和圆的关系
void isInCircle(Circle &c,Point &p)
{
int dist=(c.getCer().getX()-p.getX()) * (c.getCer().getX()-p.getX())+
(c.getCer().getY()-p.getY()) * (c.getCer().getY()-p.getY());
int rdist=c.getR() * c.getR();
if(dist==rdist)
{
cout<<"点在圆上"<<endl;
}
else if(dist>rdist)
{
cout<<"点在圆外"<<endl;
}
else
{
cout<<"点在圆内"<<endl;
}
}
int main()
{
//创建点对象和圆对象
Point p;
Circle c;
Point center;
//设置点的坐标
p.setX(0);
p.setY(0);
//设置园的半径及圆心的坐标
c.setR(10);
center.setX(10);
center.setY(0);
c.setCer(center);
//判断点与圆的关系
isInCircle(c,p);
return 0;
}
输出如下:
分文件编写:
point.h
#pragma once//防止头文件重复包含
#include <iostream>
using namespace std;
//点类
class Point
{
private:
int p_x;
int p_y;
public:
//设置/获取X,Y
void setX(int x);
int getX();
void setY(int y);
int getY();
};
point.cpp
#include "point.h"
//点类
//设置/获取X,Y
void Point::setX(int x)//Point::的作用是声明该函数是Point作用域下的成员函数
{
p_x=x;
}
int Point::getX()
{
return p_x;
}
void Point::setY(int y)
{
p_y=y;
}
int Point::getY()
{
return p_y;
}
circle.h
#pragma once
#include <iostream>
using namespace std;
#include "point.h"
//圆类
class Circle
{
private:
int c_r;//半径
Point center;//圆心
public:
//设置/获取半径圆心
void setR(int r);
int getR();
void setCer(Point cer);
Point getCer();
};
circle.cpp
#include "circle.h"
//圆类
//设置/获取半径圆心
void Circle::setR(int r)
{
c_r=r;
}
int Circle::getR()
{
return c_r;
}
void Circle::setCer(Point cer)
{
center=cer;
}
Point Circle::getCer()
{
return center;
}
main.cpp
#include <iostream>
using namespace std;
#include "point.h"
#include "circle.h"
//判断点和圆的关系
void isInCircle(Circle &c,Point &p)
{
int dist=(c.getCer().getX()-p.getX()) * (c.getCer().getX()-p.getX())+
(c.getCer().getY()-p.getY()) * (c.getCer().getY()-p.getY());
int rdist=c.getR() * c.getR();
if(dist==rdist)
{
cout<<"点在圆上"<<endl;
}
else if(dist>rdist)
{
cout<<"点在圆外"<<endl;
}
else
{
cout<<"点在圆内"<<endl;
}
}
int main()
{
//创建点对象和圆对象
Point p;
Circle c;
Point center;
//设置点的坐标
p.setX(0);
p.setY(0);
//设置园的半径及圆心的坐标
c.setR(10);
center.setX(10);
center.setY(0);
c.setCer(center);
//判断点与圆的关系
isInCircle(c,p);
return 0;
}
4.2 对象特征
对象的初始化和清理:C++中,每个对象都有初始设置和对象销毁前的清理数据的设置。
4.2.1 构造函数和析构函数
C++中利用构造函数和析构函数对对象进行初始化和清理。这两个函数会被编译器自动调用,完成对象的初始化和清理。如果程序员不提供构造和析构,编译器会提供构造函数和析构函数,但是是空的。
构造函数:创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
语法:类名(){}
1.构造函数,没有返回值也不写void;
2.函数名与类名相同;
3.构造函数可以有参数,因此可以发生重载;
4.程序在调用对象时会自动调用构造,无须手动调用,且只调用一次。
析构函数:对象销毁前系统自动调用,执行一些清理工作。
语法:~类名(){}
1.析构函数,没有返回值也不写void;
2.函数名与类名相同,在函数名前加上符号~;
3.析构函数不可以有函数,因此不可以发生重载;
4.程序在对象销毁前会自动调用析构,无须手动调用,且只调用一次。
代码如下:
#include <iostream>
using namespace std;
class Person
{
public:
//构造函数 初始化对象
Person()
{
cout<<"Person构造函数的调用"<<endl;
}
//析构函数 销毁/清理对象
~Person()
{
cout<<"Person析构函数的调用"<<endl;
}