记录跳转

第零部分:C++ 练习汇总
第一部分:C++学习记录 基础篇
第二部分:C++学习记录 核心篇


程序的内存模型

1.内存分区模型

意义

不同区域存放的数据赋予不同的生命周期,给程序员更大的灵活编程

分类

①.代码区:存放函数体的二进制代码,由操作系统进行管理
①.全局区:存放全局变量和静态变量以及常量
③.栈区:由编译器自动分配释放,存放函数的参数值、局部变量等
④.堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

2.程序运行前

【注:在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域。】

①.代码区:

  1. 存放CPU执行的机器指令(由我们写的代码翻译成的二进制代码)
  2. 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
  3. 代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令

说人话:代码区是皇上为了节省在每位嫔妃宫中焚香的财力(减少多次运行消耗内存)而放在御花园的共享香炉,谁都可以享用(执行程序的共享性),但谁也不能挪动(只读性)

②.全局区

  1. 全局区包含变量区和常量区
  2. 全局变量、静态变量、字符串常量和const修饰的全局变量(全局常量)存放在此
  3. 局部变量、const修饰的局部变量(局部常量)不在全局区,在栈区
  4. 该区域的数据在程序结束后由操作系统释放
  5. 所有函数体外的变量是全局变量
    【注:写在main函数体内的都是局部变量。】

3.程序运行后

①.栈区

  1. 由编译器自动分配释放,存放函数的参数(形参)值和局部变量等
  2. 不要返回局部变量的地址,因为栈区创建的局部变量及其值在函数执行完后由编译器自动释放/覆盖,程序失去了对局部变量在栈区内存的控制权,局部变量返回的地址对应的栈区内存可能被覆盖,main函数中新的指针指向的其返回的栈区地址解引用获得的值不可控
    (如果没被及时覆盖则可以正常接受,但也最多接受一次其内存便会被擦除)

②.堆区

  1. 由程序员分配释放。
  2. 程序运行期间,程序员不释放,堆区开辟内存就不释放。
  3. 程序结束后由操作系统回收
  4. 在C++中主要利用new在堆区开辟内存
    通过 new 数据类型 ( 值 ); 得到指向堆区内存地址的指针值,再存放于创建的指针中 指针类型 指针名 = new 数据类型 ( 值 ); 再用main函数中新的指针变量获取该返回的指针 指针类型 指针名 = 函数名( ); 即可解决局部变量不可以返回的问题
    【注:不用new,main函数新指针指向的返回值是栈区的内存,一会就释放了/被覆盖了,多次解引用接受的值不一样;但用new拿到的返回值是堆区的地址,堆区地址释放与否由程序员决定,不覆盖则多次解引用接受的值一样。】

4.new运算符

语法

①.开辟堆区地址并被指向于函数指针:

指针类型 指针名 = new 数据类型 ( 值 );
开辟数组内存:指针类型 指针名 = new 数据类型 [ 元素数目 ];
【注:堆区开辟数组内存返回的地址是数组的首地址。】

②.释放new所开辟的堆区内存

delet 指针名;
释放数组内存:delet [] 指针名;

②.整体使用方式:

-----某一函数-----
指针类型 函数名()
{
指针类型 指针名1 = new 数据类型 ( 值 );
return 指针名1;
}

-----main函数-----
指针类型 指针名2 = 函数名();
cout << *指针名2 ;

delet 指针名2;

C++中的引用

1.语法

数据类型& 新变量名 = 原变量名;
引用类型 新变量名 = 原变量名;
引用类型 引用名 = 原变量名;

注意

①.引用类似指针,存放的是地址
①.引用时,新变量必须初始化为一个原变量
②.引用后,新变量不可以再次初始化成另一个变量,相当于一个指针常量

2.引用做函数形参(引用传递)

作用

函数传参时,可以利用引用做函数形参让形参修饰实参,类似地址传递,但简化了指针修改实参(无需解引用)

指针:“爱会消失对不对?”

实践

void swap(int& c , int& d)
{
 int temp = c;
 c = d;
 d = temp;
 cout <<"c和d的地址分别为:"<< &c << "和" << &d;
}

int a = 10;
int b = 20;
 cout <<"a和b的地址分别为:"<< &a << "和" << &b;
swap(a, b);
cout << "引用传递交换后main函数内a和b分别为:" << a <<"和"<< b << endl;

注意

①.形参与实参:

  1. 值传递:独立两个变量、地址不同
    https://blog.shakuameji.com/archives/cstudy1#值传递
  2. 地址传递:独立两个变量、地址相同
    https://blog.shakuameji.com/archives/cstudy1#指针和函数(地址传递)
  3. 引用传递:绑定两个变量、地址相同

3.引用做函数返回值

语法

int& test()
{
 static int a = 10;
 return a;
}

int& b = test();

注意

  1. 不要直接返回局部变量的引用,但可以通过将局部变量改为静态变量,由栈区存放到全局区(全局区上的数据在程序结束后系统释放)来返回局部变量值。
  2. 函数和int& b = test();可以理解为:
    test()作为a的引用名返回
    int& test() = a;
    b作为test()的引用名,test()作为a的引用名。因此b,test(),a互相绑定,地址相同
    int& b = test() = a;

4.函数的调用可以作为左值

语法

int& test()
{
 static int a = 10;
 return a;
}

int& b = test();
test() = 1000;

【注:test() = 1000;意味着把a,test()和b都赋值为1000。】
【注:能取到地址的便是左值。】

5.引用的本质——指针常量

语法

数据类型& 新变量名 = 原变量名;                  
              ↓
指针类型 const 新变量名  = &原变量名;
新变量名 = 值;
      ↓
* 引用名 = 值;

注意

  1. 指针常量:该指针是常量,指向的地址不可改,地址对应的内存可以更改
  2. 常量指针:指向常量地址的指针,可以指向别的地址,但当前指向的常量地址的内存不可更改

6.常量引用

作用

在函数形参列表中,可以加const修饰形参,防止形参修饰实参

语法

①.普通用法:
const 引用类型 引用名 = 值;
②.常量引用做形参:

数据类型 函数名(const 引用类型 形参名)
{ 
函数体语句
return 表达式
}

函数名(实参名);

函数高级

1.默认参数

语法

返回值类型 函数名 ( 数据类型 形参 = 值 ) { }

注意

  1. 若函数有多个参数,要把默认参数写在形参列表最右边
  2. 默认形参值可以被调用函数时传入值覆盖
  3. 函数声明和实现只能有一处默认参数,避免参数二义性,如:
int func ( int a = 10);

int func ( int a ) { }

2.占位参数

语法

返回值类型 函数名 ( 数据类型 ) { }

注意

  1. 占位参数还可以有默认参数,如:返回值类型 函数名 ( 数据类型 = 值 ) { }

3.函数重载

作用

满足条件时可以让函数名相同,提高复用性

条件

①.函数名称相同
②.同一个作用域(如全局)下
③.函数参数类型不同,或者个数不同,或者顺序不同

注意

①.返回值类型不可以作为函数重载的条件,也就是两个函数的参数列表都一样只是返回值类型不同不能函数重载
②.引用可以作为函数重载条件,如:

void func ( int& a) { }  //函数1
void func ( const int& a) { }  //函数2
  1. 若想次调用函数1,调用时的实参要求是变量

  2. 若想次调用函数2,调用时的实参要求是常量或值

  3. 当实参是变量时:
    输入给函数的值应当是可读可写的,因此调用函数1;
    当无函数1时,函数2也可调用

  4. 当实参是常量/值时:
    若调用函数1,则意味着int& a = 常量/值,值在常量区(全局区),但引用只能储存栈/堆区地址,因此不合法;
    若调用函数2,则意味着const int& a = 常量/值,局部常量在栈区,引用合法;编译器为值创建了一个临时变量(栈区),因此是合法的 (此处涉及常量引用const 引用类型 引用名 = 值;

③.函数重载时避免使用默认参数,因为容易出现二义性,即多个函数都可以被同一句函数调用执行


类和对象

1.封装

意义

①.封装将属性和行为作为一个整体,表现生活中的事物
②.封装将属性和行为加以权限控制
【注:封装是C++面向对象的三大特性之一。】

语法

①.在设计类时,属性和行为写在一起表现事物:

class 类名 
{
public:   //访问权限-公共

  数据类型 变量名;  //属性
    
  返回值类型 行为名 ( 形参 )  //行为
  {
    变量名 = 形参;  //通过值传递赋值
    return 表达式;
  }
};

类名 对象名;  //通过类创建具体对象
对象名.变量名 = 值;  //为对象的属性赋值
对象名.行为名 ( 值 );  //值传递赋值
对象名.行为名;  //执行类中行为

②.将属性和行为放在不同权限下加以控制:

//公共权限  
public:  

//保护权限
protected:

//私有权限
private:

实践

①.尝试设计一个学生类,属性有姓名和学号,给姓名和学号用两种方法赋值,可以显示学生的姓名和学号?

class student
{
public:
  string name;
  int id;
  
  void show ()
  {
   cout<<name<<endl<<id;
   retrun 
  }
  
  void setid ( int set_id )
  { 
   id = set_id;
  }
};


student stu1;
stu1.name = "雨心";
stu1.setid(2021364401);

stu1.show();

②.尝试设计立方体类(Club),求出立方体的面积和体积,分别用全局函数和成员函数判断两个立方体是否相等?

class Cube
{
public:
 void set_L_W_H(int L, int W, int H)
 {
  m_L = L;
  m_W = W;
  m_H = H;
 }
 int get_L_W_H()
 {
  return m_L<<m_W<<m_H;
 }
 int calculateS()
 {
  return 2*m_L*m_W+2*m_L*m_H+2*m_W*m_H;
 }
 int calculateV()
 {
  return m_L*m_W*m_H;
 }
 
 //成员函数
 bool isSame2(Cube &c)
 {
 if(m_L<<m_W<<m_H == c.get_L_W_H() )
  {
  return true;
  }
 return false;
 }
private:
 int m_L;
 int m_W;
 int m_H;
};

//全局函数
bool isSame(Cube &c1, Cube &c2 )
{
 if(c1.get_L_W_H() == c2.get_L_W_H() )
 {
  return true;
 }
 return false;
}

Cube c1;
Cube c2;
c1.set_L_W_H(1, 2, 3);
c2.set_L_W_H(1, 2, 3);
cout<<"c1的面积是:"<<c1.calculateS()<<endl;
cout<<"c1的体积是:"<<c1.calculateV()<<endl;

//利用全局函数判断
bool ret = isSame(c1,c2);
if(ret)
{
 cout<<"全局函数判断:c1和c2相等"<<endl;
}
else
{
 cout<<"全局函数判断:c1和c2不相等"<<endl;
}

//利用成员函数判断
ret = c1.isSame2(c2);
if(ret)
{
 cout<<"成员函数判断:c1和c2相等"<<endl;
}
else
{
 cout<<"成员函数判断:c1和c2不相等"<<endl;
}

③.尝试设计圆形类(Circle),和一个点类(Point),计算点和圆的关系?

class point
{
private:
    int p_x;
    int p_y;

public:
    void set_point(int x,int y)
    {
        p_x = x;
        p_y = y;
    }

    int get_x(point p) 
    {
        return p.p_x;
    }
    int get_y(point p)
    {
        return p.p_y;
    }
};

class circle
{
private:
    int c_x;
    int c_y;
    int c_r;

public:
    void set_circle(int x,int y,int r)
    {
        c_x = x;
        c_y = y;
        c_r = r;
    }

    int get_x(circle c)
    {
        return c.c_x;
    }
    int get_y(circle c)
    {
        return c.c_y;
    }
    int get_r(circle c)
    {
        return c.c_r;
    }
};

float get_distance(point p, circle c)
{
    return ((p.get_x(p) - c.get_x(c)) ^ 2 - (p.get_y(p) - c.get_y(c)) ^ 2) ^ (1 / 2);
}


int isIn(point p,circle c)
{

    float distance = get_distance(p,c);

    if (distance < c.get_r(c))
    {
        return 1;
    }
    else if(distance == c.get_r(c))
    {
        return 2;
    }
    else if(distance > c.get_r(c))
    {
        return 3;
    }
}

int main()
{
    point p;
    int p_x;
    int p_y;

    cout << "请输入点的x坐标" << endl;
    cin >> p_x;
    cout << "请输入点的y坐标" << endl;
    cin >> p_y;
    p.set_point(p_x, p_y);


    circle c;
    int c_x;
    int c_y;
    int c_r;

    cout << "请输入圆心的x坐标" << endl;
    cin >> c_x;
    cout << "请输入圆心的y坐标" << endl;
    cin >> c_y;
    cout << "请输入圆的半径" << endl;
    cin >> c_r;
    c.set_circle(c_x, c_y,c_r);

    cout << "点与圆的关系为:";
    switch (isIn(p,c))
    {
       case 1:
           cout << "点在圆内";
           break;
       case 2:
           cout << "点在圆上";
           break;
       case 3:
           cout << "点在圆外";
           break;
       default:
           cout << "isIn判断出错";
           break;
    }
}

【注:类1中可以让类2作为成员变量类型。】
【注:用函数为类内变量赋值时,函数的形参名和类内变量名不能相同。】

注意

①.类中的属性和行为,我们统一称为成员
类中属性又称:成员属性、成员变量
类中行为又称:成员函数、成员方法
②.三种权限区别:

  1. public:类内成员、类外都可以访问
  2. protected:类内成员可以访问;类外不可以访问;子可以访问父中内容
  3. private:类内成员可以访问;类外不可以访问;子不能访问父中内容

③.结构体和类的区别:

结构体
默认访问权限: public private

④.成员属性设置为私有,优点如下:

  1. 可以自己控制读写权限
可读可写 只读 只写
函数形参:
函数输出:
  1. 检测写入数据有效性:在成员函数内添加条件语句并设置有效条件
  2. 左移(<<)/右移(>>)运算符:
    图片-1660206396082

⑤.项目过大时要把类拆成独立于项目主源文件外的一套头文件和源文件

  1. 头文件仅包含声明,如:
#program once
#include <iosteram>
using namespace std;
#include "调用到的类名.h";

class 类名 
{
public:   
  返回值类型 行为名 ( 形参 );
private:
  数据类型 变量名;
};

【注:如果类1内的成员变量类型涉及类2,则类1头文件最开头要包含类2的头文件,如:#include "类2名.h";
2. 源文件仅包含函数体语句,如:

#include "类名.h"

返回值类型 类名::行为名 ( 形参 ) 
  {
    变量名 = 形参;  
    return 表达式;
  }

【注:源文件要在最开头包含对应的头文件】
【注:源文件所有成员函数行为名前都要加作用域,如: 类名::

  1. 主源文件要包含头文件,如:
#include "类名.h";

2.对象的初始化和清理

构造函数和析构函数

作用

①.构造函数:作用在创建对象时对成员属性赋值
②.析构函数:作用在对象销毁前执行清理工作

语法

①.构造函数:

类名 ( ) { }

【注:没有返回值也不写void,构造函数名和对应的类名一致,可重载。】
②.析构函数:

~ 类名 () { }

【注:没有返回值也不写void,析构函数名和对应的类名一致,没有参数因此不可重载。】

注意

  1. 构造函数和析构函数完成对象初始化和清理工作,这两个函数由编译器自动调用
  2. 如果我们不提供构造和析构,编译器会提供空实现的构造函数和析构函数
  3. 构造函数和析构函数都写在对应的类内。

构造函数的分类及调用

分类方式

①.按参数分:有参构造(默认构造)、无参构造
②.按类型分:普通构造、拷贝构造

调用方式

①.括号法
②.显示法
③.隐式转换法

语法

①.拷贝构造:

类2名 ( const 类1名& 对象名 ) 
{
 类2成员变量 = 对象名.类1成员变量;
}

【注:用已有的类1对象初始化类2中的新对象。】
②.括号法:

类名 对象名;   //调用默认构造函数
类名 对象名 ( 变量名/值 );   //调用有参构造函数
类名 对象名 ( 对象名 );   //调用拷贝构造函数

③.显示法:

类名 对象名;   //调用默认构造函数
类名 对象名 = 类名( 变量名/值 );   //调用有参构造函数
类名 对象名 = 类名( 对象名 );   //调用拷贝构造函数

④.隐式转换法:

类名 对象名;   //调用默认构造函数
类名 对象名 = 变量名/值;   //调用有参构造函数
类名 对象名 =  对象名;   //调用拷贝构造函数

注意

  1. 调用默认构造函数不要加括号,否则会被编译成一条函数声明
  2. 匿名对象:创建匿名对象行执行结束后立即回收匿名对象,语法类名 (值);
  3. 不要用拷贝构造函数初始化匿名对象,否则会被编译成一条对象声明

拷贝构造函数调用情况

类别

①.使用一个已经创建完毕的对象来初始化一个对象

类名 对象1名 ( 变量/值 );
类名 对象2名 ( 对象1名 );

②.值传递的方式给函数参数传值

void 函数名 ( 类名 形参对象名){}


类名 对象1名 ;
函数名 ( 对象1名 )

③.值方式返回局部对象

类名 函数名()
{
 类名 对象名;
 return 对象名;
}

构造函数调用规则

  1. 创建一个类,C++编译器会给每个类添加至少3个函数:
    ①.默认构造(空实现)
    ②.析构函数(空实现)
    ③.拷贝构造(值拷贝)
  2. 如果我们写了有参构造函数,编译器就不再提供默认构造,这时不允许调用默认构造;
    但编译器依然提供拷贝构造,仅有值拷贝作用
  3. 如果我们写了拷贝构造,编译器就不提供其他构造函数

深拷贝与浅拷贝

作用

①.深拷贝:在堆区重新申请空间,进行拷贝操作
②.浅拷贝:简单的赋值拷贝操作

语法

class 类名 
{
public:

}