0


C++核心编程(持续更新)

1、内存分区模型

C++在程序执行时, 内存可分为4个区域:

  • 代码区: 存放函数体二进制代码,由操作系统进行管理,我们写的所有代码都在这个区域中有体现
  • 全局区: 存放全局变量和静态变量以及常量
  • 栈区: 由编译器自动分配释放,存放函数的参数值,局部变量等
  • 堆区: 由程序员分配和释放,如果程序员不释放那么程序结束时由操作系统释放

内存四区的意义: 内存的四个区域,生命周期各不相同,让我们的编程可以更灵活。

四个区域主要可以体现在程序运行前和程序运行后:

1.1程序运行前

在程序编译后,生成exe可执行程序,未执行该程序前可以分为两个区域:

代码区:存放CPU执行的机器指令。

代码区的两个特点:

  • 共享,共享的母的是对于频繁执行的程序,只需要在内存中有一份代码即可
  • 只读,防止程序意外的修改程序中的代码

全局区:全局变量和静态变量存放在该区域。

全局区还包含了常量区,字符串常量和其他常量同样存放在全局区,该区域的数据在程序结束后由操作系统释放

其他数据类型是否在全局区可以使用代码进行测试,像下面一段代码是测量常量字符串、const修饰的全局变量、全局变量、static修饰的静态变量是否在同一区域:

#include <iostream>
using namespace std;
int a = 10;
const int c = 10;
int main()
{
    cout << (int)&a << endl;
    static int b = 20;
    cout << (int)&b << endl;
    cout << (int)&"zhangsan" << endl;
    cout << (int)&c << endl;
     return 0;
}

运行结果为:4759552 4759556 4750132 4750128

从这个运行结果其实就能够直观的看出这些数据在同一个区域,因为他们的内存编号转化为十进制之后相差不大;当然,其他区域的数据是否也在全局区或者其他区域这个可以自行测量。

1.2程序运行后

栈区:由编译器自行分配和释放,存放函数的参数,局部变量等

使用栈区时的注意事项:不要返回局部变量的地址(否则会造成野指针问题),栈区数据的开辟由编译器自动释放。

分析一下下面这段程序,这段程序有什么错误?

int * func()
{
    int a = 10;
    return &a;
}

int main() {

    int *p = func();

    cout << *p << endl;
    cout << *p << endl;

    system("pause");

    return 0;
}

这段程序很明显是有问题的,对于函数func中创建的局部变量,在返回主函数之后局部变量的内存空间自动销毁,返回继续进行解引用操作的话势必会造成野指针的问题。

堆区: 由程序员分配和释放,如果程序员不释放,程序结束的时候由操作系统回收释放

C++中怎么在堆区开辟内存

使用new关键字

1.在堆区开辟一个整型的空间 int* p = new int(10);

2.在堆区开辟一个整型的数组 int* p = new int[10];

当然有数据的开辟,就有数据的销毁,销毁堆区开辟的空间时使用delete关键字;

程序举例:

int* func()
{
    int* a = new int(10);
    return a;
}

int main() {

    int *p = func();

    cout << *p << endl;
    cout << *p << endl;

    //利用delete释放堆区数据
    delete p;

    //cout << *p << endl; //报错,释放的空间不可访问

    return 0;
}

2、引用

2.1引用的基本类型

作用:给变量起别名

语法:数据类型 &别名 = 原名

#include <iostream>
using namespace std;
int main()
{
    int a = 10;
    int& b = a;
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;

    a = 20;
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    return 0;
}

a和b指向的是同一块内存空间,a的值改变b的值也随之改变。

2.2引用的注意事项

  • 引用必须初始化
  • 引用初始化之后不能改变
注意:在使用引用的时候,一定不能写成这样
int& b;
b = a;
第一,引用在使用的时候必须进行初始化;
第二,b = a,进行的是赋值操作;

2.3引用作为函数参数

作用:函数传参的时候,可以利用引用的技术让形参修饰实参

有点:可以简化指针修改实参

下面是分别使用值传递、址传递、引用传递进行交换的实例:

//1. 值传递
void mySwap01(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
}

//2. 地址传递
void mySwap02(int* a, int* b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

//3. 引用传递
void mySwap03(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {

    int a = 10;
    int b = 20;

    mySwap01(a, b);
    cout << "a:" << a << " b:" << b << endl;

    mySwap02(&a, &b);
    cout << "a:" << a << " b:" << b << endl;

    mySwap03(a, b);
    cout << "a:" << a << " b:" << b << endl;

    system("pause");

    return 0;
}

从址传递和引用传递不难看出这两种的作用效果是相同的,但是引用的代码更简洁一点。

2.4引用作为函数的返回值

作用:引用可以作为函数的返回值存在。

注意:一定不要返回局部变量的引用 函数调用可以作为左值

//返回静态变量引用
int& test02() {
    static int a = 20;
    return a;
}

int main() {

    //不能返回局部变量的引用
    int& ref = test01();
    cout << "ref = " << ref << endl;
    cout << "ref = " << ref << endl;

    //如果函数做左值,那么必须返回引用
    int& ref2 = test02();
    cout << "ref2 = " << ref2 << endl;
    cout << "ref2 = " << ref2 << endl;

    test02() = 1000;

    cout << "ref2 = " << ref2 << endl;
    cout << "ref2 = " << ref2 << endl;

    return 0;
}

函数调用作为左值的时候相当于一个数的别名,同样可以对此数据进行改变。

2.5引用的本质

本质:引用的本质在C++内部是实线是一个指针常量

我们之前所说的指针变量运用的主要场景是 数据类型* p = &a;

引用的本质其实也是指针,只不过这个指针是一个常量,其中的数据是不能更改的;

也就是const 数据类型 *p = &a;

2.6常量的引用

作用:常量的引用主要是限制数据只是读的,防止对数据进行误操作。

在引用数据的前面加const修饰

//引用使用的场景,通常用来修饰形参
void showValue(const int& v) {
    //v += 10;
    cout << v << endl;
}
//修饰形参,使形参的内容不可以改变

3、函数提高

3.1函数默认的参数

在C++中,函数的形参列表中的形参是可以有默认值的。

语法:返回值类型 函数名(形参 = 默认值) {}

int sum(int a , int b = 20, int c = 30)
{
    return a + b + c;
}

int main()
{
    
    int ret = sum(10, 20);
    cout << ret << endl;
    return 0;
}

使用函数默认参数时的注意点:

1.如果某个位置开始函数的参数时默认值,那么从这个位置开始从左往右,都需要有默认参数。

2.如果函数的生命有默认值,函数的实现中不能有默认值。

int sum(int a, int b, int c = 10)//这种请款是不允许的
int sum(int a, int b, int c = 10)
{
    return a + b + c;
}

int main()
{
    
    int ret = sum(10, 20);
    cout << ret << endl;
    return 0;
}

3.2函数占位参数

C++中函数的形参列表里面可以有占位参数,用来做占位,调用函数的时候必须填补占位参数的位置。

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

//函数占位参数 ,占位参数也可以有默认参数
void func(int a, int) {
    cout << "func函数的调用" << endl;
}

int main() {

    func(10,10); //占位参数必须填补

    return 0;
}

3.3函数重载

3.3.1函数重载的概述

函数重载:函数名可以相同,提高复用性

函数重载需要满足的条件:

  • 同一作用域
  • 函数名相同
  • 函数参数不同(个数,类型,顺序满足其一即可)

注意:函数的返回值不能作为函数重载的条件。

构成重载的三种情况(满足以上一种的):

1.参数个数不同:

int add(int a, int b);
int add(int a);

**2.参数类型不同: **

int add(int a, int b);
int add(double a, double b);

**3.参数顺序不同: **

int add(int a, double b);
int add(double a, int b);

3.3.2函数重载得注意事项

  • 引用作为重载条件
  • 函数重载碰到默认参数
//引用作为重载条件
void func(int &a)
{
    cout << "func (int &a) 调用 " << endl;
}

void func(const int &a)
{
    cout << "func (const int &a) 调用 " << endl;
}
//调用有const修饰的引用和无const修饰的引用是不同的,可以实现重载
func(a); //调用无const
func(10);//调用有const
//函数重载中的默认参数
void func2(int a, int b = 10)
{
    cout << "func2(int a, int b = 10) 调用" << endl;
}

void func2(int a)
{
    cout << "func2(int a) 调用" << endl;
}

func2(10); //这样写以上两种func2函数都可以调用,这种写法容易造成歧义,不建议这样写

4、类和对象

C++面向对象有三大特性:封装、继承和多态

C++认为万事万物皆为对象,对象上有其属性和行为。

例如:人可以作为对象,属性有姓名、身高、体重等;行为有走、跑、吃饭、睡觉。

车可以作为对象,属性有轮胎、方向盘、车灯等;行为有放音乐、开空调等。

4.1封装

4.1.1封装的意义

  • 将属性和行为作为一个整体,表现生活中的事物
  • 将属性和行为加以权限控制

封装的意义一:

在设计类的时候,属性和行为写在一起,表现事物。

语法:class 类名{ 访问权限: 属性/行为};

示例:设计一个圆类,求圆的周长

示例代码:

//圆周率
const double PI = 3.14;

class Circle
{
public:  //访问权限  公共的权限

    //属性
    int m_r;//半径

    //行为
    //获取到圆的周长
    double calculateZC()
    {
        //2 * pi  * r
        //获取圆的周长
        return  2 * PI * m_r;
    }
};

int main() {

    //通过圆类,创建圆的对象
    // c1就是一个具体的圆
    Circle c1;
    c1.m_r = 10; //给圆对象的半径 进行赋值操作

    //2 * pi * 10 = = 62.8
    cout << "圆的周长为: " << c1.calculateZC() << endl;
    return 0;
}

封装的意义二:

类在设计时,可以把属性和行为放在不同的权限下,加以控制。

访问权限有三种:

  1. public 公共权限 类内可以访问,类外也可以访问
  2. protected 保护权限 类内可以访问,类外不可以访问
  3. **private 私有权限 类内可以访问,类外不可以访问 **

注意:protected和private两种权限是不同的,主要体现在继承中。

4.1.2struct和class的区别

在C++中struct和class唯一的区别就是在于默认的访问权限不同

  • struct默认权限为公共
  • class默认权限为私有 class使用默认权限时类外不能访问

4.1.3成员属性设置为私有

优点:

  • 将所有成员属性设置为私有,可以控制读写权限
  • 对于写权限,可以检测数据的有效性
class Person {
public:

    //姓名设置可读可写
    void setName(string name) {
        m_Name = name;
    }
    string getName()
    {
        return m_Name;
    }

    //获取年龄 
    int getAge() {
        return m_Age;
    }
    //设置年龄
    void setAge(int age) {
        if (age < 0 || age > 150) {
            cout << "你个老妖精!" << endl;
            return;
        }
        m_Age = age;
    }

    //情人设置为只写
    void setLover(string lover) {
        m_Lover = lover;
    }

private:
    string m_Name; //可读可写  姓名
    
    int m_Age; //只读  年龄

    string m_Lover; //只写  情人
};

4.2对象的初始化和清理

  • 生活中我们买的电子产品都会有出厂设置,如果有一天我们不用的话可以对信息进行删除
  • C++中的面向对象来源于生活,每个对象都会设置初始值最后进行销毁

4.2.1构造函数和析构函数

对象的初始化和清理是两个非常重要的安全问题,一个对象或者变量没有初始化状态的话,那么造成的后果是严重的。

同样的使用完一个对象或者变量,没有及时清理的话,也会造成安全问题。

C++利用构造函数和析构函数来解决以上问题,这两个函数由编译器自动调用,完成对象的初始化和清理工作;

我们不需要提供构造和析构函数,编译器会提供构造和析构函数的空实现。

构造函数语法:类名(){}

  • 构造函数,没有返回值也不写void
  • 函数名和类名相同
  • 构造函数可以有参数,因此可以发生重载
  • 程序在调用对象的时候自动进行构造,无需手动调用,而且只调用一次

析构函数的语法:~类名(){}

  • 析构函数也是没有返回值的,也不写void
  • 函数名和类名相同,在名称的前面加上~
  • 析构函数不可以有参数,因此不可以发生重载
  • 程序在对象销毁前自动调用析构函数,无需手动调用,而且只调用一次
class Person
{
public:
    //构造函数
    Person()
    {
        cout << "Person的构造函数调用" << endl;
    }
    //析构函数
    ~Person()
    {
        cout << "Person的析构函数调用" << endl;
    }

};

4.2.2构造函数的分类和调用

两种分类方式:

按照参数可以分为:有参构造和无参构造

按照类型可以分为:普通构造和拷贝构造

三种调用方式:

括号法

显示法

隐式转换法

    //2.1  括号法,常用
    Person p1(10);
    //注意1:调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明
    //Person p2();

    //2.2 显式法
    Person p2 = Person(10); 
    Person p3 = Person(p2);
    //Person(10)单独写就是匿名对象  当前行结束之后,马上析构

    //2.3 隐式转换法
    Person p4 = 10; // Person p4 = Person(10); 
    Person p5 = p4; // Person p5 = Person(p4); 

    //注意2:不能利用 拷贝构造函数 初始化匿名对象 编译器认为是对象声明
    //Person p5(p4);

4.2.3拷贝构造函数调用时机

C++中拷贝函数调用时机通常有三种情况:

  • 使用一个已经创建完毕的对象来初始化一个新对象
  • 值传递的方式给函数参数传值
  • 以值方式但会局部对象

三种情况的代码实例:

//1. 使用一个已经创建完毕的对象来初始化一个新对象
void test01() {

    Person man(100); //p对象已经创建完毕
    Person newman(man); //调用拷贝构造函数
    Person newman2 = man; //拷贝构造

    //Person newman3;
    //newman3 = man; //不是调用拷贝构造函数,赋值操作
}
//2. 值传递的方式给函数参数传值
//相当于Person p1 = p;
void doWork(Person p1) {}
void test02() {
    Person p; //无参构造函数
    doWork(p);
}
//3. 以值方式返回局部对象
Person doWork2()
{
    Person p1;
    cout << (int *)&p1 << endl;
    return p1;
}

void test03()
{
    Person p = doWork2();
    cout << (int *)&p << endl;
}

4.2.4构造函数调用规则

默认情况下,C++编译器至少给一个类添加3个函数

  • 默认构造函数(无参,函数体为空)
  • 默认析构函数(无惨,函数体为空)
  • 默认拷贝函数,对属性进行拷贝

构造函数的调用规则如下:

  • 如果用户定义有参构造函数,C++不会提供默认构造函数,但是会提供拷贝函数
  • 如果用户定义拷贝构造函数,C++不会再提供其他构造函数

4.2.5深拷贝和浅拷贝

  • 浅拷贝:简单的赋值拷贝操作
  • 深拷贝:在堆区重新申请空间,进行拷贝操作
class Person {
public:
    //无参(默认)构造函数
    Person() {
        cout << "无参构造函数!" << endl;
    }
    //有参构造函数
    Person(int age ,int height) {
        
        cout << "有参构造函数!" << endl;

        m_age = age;
        m_height = new int(height);
        
    }
    //拷贝构造函数  
    Person(const Person& p) {
        cout << "拷贝构造函数!" << endl;
        //如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
        m_age = p.m_age;
        m_height = new int(*p.m_height);
        
    }

    //析构函数
    ~Person() {
        cout << "析构函数!" << endl;
        if (m_height != NULL)
        {
            delete m_height;
        }
    }
public:
    int m_age;
    int* m_height;
};

在类的拷贝函数中会存在一个问题:

进行拷贝之后,这两个对象指向的是同一块空间,而析构函数在每个对象销毁时都会执行一次,这就会造成堆区空间重复释放的问题,解决办法就是使用深拷贝,在堆区开辟不同的空间。

4.2.6初始化列表

作用:C++提供了初始化列表的语法,用来初始化属性。

语法:构造函数(): 属性1(值1), 属性2(值2)...{}

使用代码举个栗子:

class Person {
public:
    //初始化列表方式初始化
    Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) {}
    void PrintPerson() {
        cout << "mA:" << m_A << endl;
        cout << "mB:" << m_B << endl;
        cout << "mC:" << m_C << endl;
    }
private:
    int m_A;
    int m_B;
    int m_C;
};

4.2.7类对象作为类成员

C++类中的成员可以是另一个类的对象,我们称该成员为对象成员。

class A {}
class B
{
    A a;
}

这样调用有一个顺序的问题,是先调用A的构造还是先调用B的构造,是先调用A的析构还是先调用B的析构?

class Phone
{
public:
    Phone(string name)
    {
        m_PhoneName = name;
        cout << "Phone构造" << endl;
    }

    ~Phone()
    {
        cout << "Phone析构" << endl;
    }

    string m_PhoneName;

};

class Person
{
public:

    //初始化列表可以告诉编译器调用哪一个构造函数
    Person(string name, string pName) :m_Name(name), m_Phone(pName)
    {
        cout << "Person构造" << endl;
    }

    ~Person()
    {
        cout << "Person析构" << endl;
    }

    void playGame()
    {
        cout << m_Name << " 使用" << m_Phone.m_PhoneName << " 牌手机! " << endl;
    }

    string m_Name;
    Phone m_Phone;

};
void test01()
{
    Person p("张三" , "苹果X");
    p.playGame();
}

调用test01函数我们就会发现,初始化的时候先调用的是A的构造函数,然后再调用的B的构造函数;

进行销毁的时候,先调用的是B的析构函数,再调用的是A的析构函数。

这个循序我们可以把A比作一个汽车的零件,把B比作汽车,在组装的时候,肯定要先构造零件,再构造汽车;

在拆掉汽车的时候,要先把汽车拆掉,才能进一步拆除汽车的零件。

4.2.8静态成员

静态成员就是在成员变量和成员函数前面加上关键字static,称之为静态成员。

静态成员包括:

静态成员变量

  • 所有对象共享同一份数据
  • 在编译阶段分配内存
  • 类内生命,类外初始化

静态成员函数

  • 所有对象共享同一个函数
  • 静态成员函数只能访问静态成员变量

类内生命,类外初始化:

class Person
{
    
public:

    static int m_A; //静态成员变量

private:
    static int m_B; //静态成员变量也是有访问权限的
};
int Person::m_A = 10;
int Person::m_B = 10;

静态成员变量在全局区,使用时使用对象和类名都能够访问到。

静态成员函数:

class Person
{

public:
    
    static void func()
    {
        cout << "func调用" << endl;
        m_A = 100;
        //m_B = 100; //错误,不可以访问非静态成员变量
    }

    static int m_A; //静态成员变量
    int m_B; // 
private:
    //静态成员函数也是有访问权限的
    static void func2()
    {
        cout << "func2调用" << endl;
    }
};

静态成员函数只能访问静态成员变量;

4.3C++对象模型和this指针

4.3.1成员变量和成员函数分开存储

在C++中,类内的成员变量和成员函数分开存储,只有非静态的成员变量才属于类的对象上

class Person {
public:
    Person() {
        mA = 0;
    }
    //非静态成员变量占对象空间
    int mA;
    //静态成员变量不占对象空间
    static int mB; 
    //函数也不占对象空间,所有函数共享一个函数实例
    void func() {
        cout << "mA:" << this->mA << endl;
    }
    //静态成员函数也不占对象空间
    static void sfunc() {
    }
};

int main() {

    cout << sizeof(Person) << endl;//只有非静态的成员变量属于对象,所以输出为4

    return 0;
}

4.3.2this指针的概念

每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会公用同一块代码;;

那么问题就是:这一块代码是如何区分是哪个对象调用了自己呢?

C++通过提供特殊的对象指针,this指针,解决了上述问题。

this指针指向被调用的成员函数所属的对象,this指针是隐含每一个非静态成员函数的一种指针。

this指针是不需要定义的,可以直接使用。

this指针的两个用途:

  • 当形参和成员变量同名的时候,可以用this指针来区分(这个作用和Java中的this是相同的)
  • 在类的非静态成员函数中返回对象本身,可以使用return *this
class Person
{
public:
    Person(int age)
    {
        //1、当形参和成员变量同名时,可用this指针来区分
        this->age = age;
    }

    Person& PersonAddPerson(Person p)
    {
        this->age += p.age;
        return *this;
    }

    int age;
};

void test01()
{
    Person p1(10);
    cout << "p1.age = " << p1.age << endl;

    Person p2(10);
    p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1);
    cout << "p2.age = " << p2.age << endl;
}

int main() {

    test01();
    return 0;
}

4.3.3空指针访问成员函数

C++中空指针也是可以调用成员函数的,但是需要注意的是有没有用到this指针;

如果用到this指针,需要加以判断保证代码的健壮性。

//空指针访问成员函数
class Person {
public:

    void ShowClassName() {
        cout << "我是Person类!" << endl;
    }

    void ShowPerson() {
        if (this == NULL) {
            return;
        }
        cout << mAge << endl;
    }

public:
    int mAge;
};

void test01()
{
    Person * p = NULL;
    p->ShowClassName(); //空指针,可以调用成员函数
    p->ShowPerson();  //但是如果成员函数中用到了this指针,就不可以了
}

int main() {

    test01();
    return 0;
}

成员函数在使用this指针的时候,需要进行检查。

4.3.4const修饰成员函数

常函数:

  • 成员函数后面加const之后我们称这个函数为常函数
  • 常函数内不可以修改成员属性
  • 成员属性声明时加关键字mutable之后,在常函数中依然可以进行修改

常对象:

  • 声明对象前加const称该对象为常对象
  • 常对象只能调用常函数
class Person {
public:
    Person() {
        m_A = 0;
        m_B = 0;
    }
    //this指针的本质是一个指针常量,指针的指向不可修改
    //如果想让指针指向的值也不可以修改,需要声明常函数
    void ShowPerson() const {
        //const Type* const pointer;
        //this = NULL; //不能修改指针的指向 Person* const this;
        //this->mA = 100; //但是this指针指向的对象的数据是可以修改的

        //const修饰成员函数,表示指针指向的内存空间的数据不能修改,除了mutable修饰的变量
        this->m_B = 100;
    }

    void MyFunc() const {
        //mA = 10000;
    }

public:
    int m_A;
    mutable int m_B; //可修改 可变的
};

//const修饰对象  常对象
void test01() {

    const Person person; //常量对象  
    cout << person.m_A << endl;
    //person.mA = 100; //常对象不能修改成员变量的值,但是可以访问
    person.m_B = 100; //但是常对象可以修改mutable修饰成员变量

    //常对象访问成员函数
    person.MyFunc(); //常对象不能调用const的函数

}

int main() {

    test01();
    return 0;
}

本章完,后面会继续更新C++方面的内容,喜欢的家人们可以给个点赞,收藏+关注,谢谢大家!


本文转载自: https://blog.csdn.net/qq_63179783/article/details/125377955
版权归原作者 Catzzz666 所有, 如有侵权,请联系我们删除。

“C++核心编程(持续更新)”的评论:

还没有评论