黑马程序员C++2024新版笔记 第4章 函数和结构体

目录

1.结构体的基本应用

2.结构体成员的默认值

3.结构体数组

4.结构体指针

->操作符

5.结构体指针数组

1.引入已存在的结构体数组地址

2.通过new操作符申请指针数组空间

6.函数的概念

7.函数的基础语法

8.无返回值函数和void类型

9.空参函数

10.函数的嵌套调用

11.函数的嵌套调用练习题讲解

12.参数的值传递和地址传递

13.函数综合案例------黑马ATM

14.函数传入数组

15.函数传入数组练习题讲解------数组排序函数

16.引用的基本概念

区别引用和指针

引用语法:

17.引用传参

18.函数返回指针及局部变量

如何规避?------动态内存管理

19.static关键字

使用static关键字还是动态内存分配?

20.函数返回数组

21.函数返回数组的改进

22.函数的默认值

23.【了解】手动编译的流程和include指令

在clion集成开发环境中演示g++手动编译流程:

1.新建一个文件夹"手动编译演示"

2.切换成命令行模式

3.输入预处理指令:

4.输入编译指令:

5.输入汇编指令:

6.输入链接指令:

简易版手动编译:

24.【了解】多文件函数编程


1.结构体的基本应用

结构体struct是一种用户自定义的复合数据类型,可以包含不同类型的成员。例如:

cpp 复制代码
struct Studet
{
    string name;
    int age;
    string gender;
}

结构体的声明定义和使用的基本语法:

cpp 复制代码
struct 结构体类型
{
    成员1类型 成员1名称;
    ···
    成员n类型 成员n名称;
};

结构体定义和访问成员练习:

cpp 复制代码
#include <iostream>
using namespace std;

int main() {

    struct Student   // 自己创建的新数据类型
    {
        string name;    // 成员1 表示姓名
        int age;    // 成员2 表示年龄
        string gender;  // 成员3 表示性别
    };

    // 使用该类型创建变量
    struct Student stu;    // 结构体变量的声明可以省略struct关键字,建议写上这样便于区分自定义结构体与其他类型

    // 结构体赋值
    stu = {"周杰", 11, "man"};

    // 输出结构体
    // 错误写法 cout << stu; 因为结构体变量是一个整体的包装,无法直接cout输出
    // 需要访问每一个成员进行输出
    cout << stu.name << endl;
    cout << stu.age << endl;
    cout << stu.gender << endl;

    // 声明和赋值同步写法
    struct Student stu2 = {"lin", 20, "woman"};
    cout << stu2.name << endl;
    cout << stu2.age << endl;
    cout << stu2.gender << endl;

    return 0;
}

2.结构体成员的默认值

设计结构体时可以给成员设置一个默认值,例如:

cpp 复制代码
struct Student {
        string name;
        string major_code = "083032";  // 默认专业代码
        int dormitory_num = 1;         // 默认分配1号宿舍
    };

    struct Student s1 = {"周末轮"};
    // 不想使用默认值可以自己赋值
    struct Student s2 = {"林军杰", "083082",  3};

代码演示本节内容:

cpp 复制代码
#include <iostream>
using namespace std;

int main() {

    struct Student {
        string name;
        string major_code = "083032";  // 默认专业代码
        int dormitory_num = 1;         // 默认分配1号宿舍
    };

    struct Student s1 = {"周末轮"};
    // 不想使用默认值可以自己赋值
    struct Student s2 = {"林军杰", "083082",  3};

    cout << "学生1姓名" << s1.name << endl;
    cout << "学生2姓名" << s2.name << endl;
    cout << "学生2专业代码" << s2.major_code << endl;
    cout << "学生2宿舍号" << s2.dormitory_num << endl;
    
    return 0;
}

3.结构体数组

结构体支持数组模式。结构体数组的语法和普通的数组基本一样,只不过需要把结构体类型代入进去:

cpp 复制代码
// 声明数组对象
[struct] 结构体类型 数组名[数组长度];
// 赋予数组的每一个元素
数组名[0]={,,,,,};
数组名[1]={,,,,,};
···

// 声明和赋值同步的快捷写法
[struct] 结构体类型 数组名 [数组长度] = {{},{},···,{}};

本节内容的代码演示:

cpp 复制代码
#include <iostream>
using namespace std;

int main() {

    struct Student {
        string name;
        string major_code = "083032";  // 默认专业代码
        int dormitory_num = 1;         // 默认分配1号宿舍
    };

    // 创建结构体数组
    Student arr[3]; //结构体数组对象声明
    arr[0] = {"张三", "083032", 1};
    arr[1] = {"李四", "083032", 2};
    arr[2] = {"王五", "083032", 3};

    for (int i = 0; i < 3; i++) {
        cout << "当前下标:" << i << "姓名是:" << arr[i].name << endl;    // 通过.访问成员
        cout << "当前下标:" << i << "专业代码是:" << arr[i].major_code << endl;
        cout << "当前下标:" << i << "宿舍号是:" << arr[i].dormitory_num << endl;
    }

    // 数组的声明和赋值同步写法
    Student students[2] = {
        {"张三", "083032", 1},
        {"李四", "234567", 0}
    };

    for (int i = 0; i < 2; i++) {
        cout << "结构体数组2的下标:" << i << "姓名是:" << students[i].name << endl;
        cout << "结构体数组2的下标:" << i << "专业代码是:" << students[i].major_code << endl;
        cout << "结构体数组2的下标:" << i << "宿舍号是:" << students[i].dormitory_num << endl;
    }
    return 0;
}

4.结构体指针

结构体不仅支持数组,同样支持指针。以下面的代码为例,结构体类型的指针p指向结构体对象的内存地址。

cpp 复制代码
struct Student {
        string name;
        string major_code = "083032";  // 默认专业代码
        int dormitory_num = 1;         // 默认分配1号宿舍
    };

struct Student stu = {"张三", "003321", 5};
struct Student *p = &stu;

由于指针指向的地址是C++管理的,也就是静态内存管理,所以指针无法被回收。如果想要回收空间,可以使用动态内存管理,通过new操作符申请一个指针/结构体空间:

cpp 复制代码
struct Student *p = new Student{"张三", "446712", 7};

->操作符

使用指针变量访问结构体成员需要更换操作符为:->

cpp 复制代码
cout << p->name << endl;
cout << p->major_code << endl;
cout << p->dormitory_num << endl;

代码演示本节知识点:

cpp 复制代码
#include <iostream>
using namespace std;

int main() {

    struct Student {
        string name;
        string major_code = "083032";  // 默认专业代码
        int dormitory_num = 1;         // 默认分配1号宿舍
    };


    // 先创建一个标准的结构体对象(静态内存管理
    struct Student stu = {"jay", "443321", 2};
    // 创建结构体的指针,指向结构体对象的地址
    struct Student * p = &stu;
    // 通过结构体指针访问结构体的成员
    cout << "结构体中成员的name: " << p->name << endl;
    cout << "结构体中成员的major_code: " << p->major_code << endl;
    cout << "结构体中成员的dormitory_num: " << p->dormitory_num << endl;
    // 这种写法的指针无法回收内存空间

    // new操作符申请结构体指针
    struct Student *p2 = new Student{"jay", "443321", 2};
    cout << "结构体中成员的name: " << p2->name << endl;
    cout << "结构体中成员的major_code: " << p2->major_code << endl;
    cout << "结构体中成员的dormitory_num: " << p2->dormitory_num << endl;

    // 释放
    delete p2;

    return 0;
}

5.结构体指针数组

结构体可以使用数组指针,主要用于动态内存分配,方便管理大量结构体占用的内存。

结构体指针数组分为2种使用方式:

1.引入已存在的结构体数组地址

cpp 复制代码
struct Student arr[] = {{"张三"},{"李四"},{"王五","009988",2}};
    // 指向已存在数组地址
    struct Student *p = arr;
    // 数组的第一个元素是结构体对象(非指针)使用,访问成员
    cout << p[0].name << endl;
    cout << p[1].name << endl;
    cout << p[2].name << endl;

这种方法相较第二种的使用更少,因为无法动态分配内存。

2.通过new操作符申请指针数组空间

cpp 复制代码
struct Student *p2 = new Student[3] {
        {"张三"},
        {"李四"},
        {"王五"}
    };

代码演示本节内容:

cpp 复制代码
#include <iostream>
using namespace std;

int main() {

    struct Student {
        string name;
        string major_code = "083032";  // 默认专业代码
        int dormitory_num = 1;         // 默认分配1号宿舍
    };


    struct Student arr[] = {{"张三"},{"李四"},{"王五","009988",2}};
    // 指向已存在数组地址
    struct Student *p = arr;
    // 数组的第一个元素是结构体对象(非指针)使用,访问成员
    // 访问结构体对象仍然用.
    cout << p[0].name << endl;  // 指针和数组一样可以使用下标,代表p指针所在的位置,0相当于没有进行任何运算,以此类推
    cout << p[1].name << endl;
    cout << p[2].name << endl;

    // 通过new操作符申请指针数组空间,定义指针接收
    struct Student *p2 = new Student[3] {
        {"张三"},
        {"李四"},
        {"王五"}
    };

    cout << "数组二第一个元素中记录的name是:" << p2[0].name << endl;
    cout << "数组二第二个元素中记录的name是:" << p2[1].name << endl;
    cout << "数组二第三个元素中记录的name是:" << p2[2].name << endl;

    // 释放内存
    delete[] p2;

    return 0;
}

6.函数的概念

函数是一个提前封装好的、可重复使用的、完成特定功能的独立代码单元。

函数的5个组成要素:返回值类型(int、bool等);函数名;参数声明;函数体;返回值。

7.函数的基础语法

cpp 复制代码
返回值类型 函数名(参数1类型 参数1名称, 参数2类型 参数2名称···){

函数体;
···

return 返回值;
}

返回值类型声明函数运行完成后提供的结果类型,与返回值相关。

返回值提供声明的结果给调用者。

函数名称即为函数名。

参数列表向函数提供待处理的数据。

函数体就是实现函数功能的代码。

函数执行流程:

  1. 调用者通过函数名调用函数,通过参数列表传输数据
  2. 函数用返回值给调用者提供结果

本节内容代码演示:

cpp 复制代码
#include <iostream>
using namespace std;

/*
 * 需求:编写一个函数,接收2个int数字传入,返回最大的那一个
 *
 */

// 自定义函数要写在main函数外
int get_max(int a, int b) {
    
}

int main() {



    return 0;
}

8.无返回值函数和void类型

函数的返回值并非是必须提供的,即可以声明函数不提供返回值。

cpp 复制代码
void say_hello(string name){
    cout << name << "你好,我是黑马程序员" << endl;
}

当函数不提供返回值时,需要:

  1. 声明函数返回值类型为void(void表示空,用于声明无返回值函数)
  2. 不需要写return语句
  3. 调用者无法得到返回值

本节内容代码演示:

cpp 复制代码
#include <iostream>
using namespace std;

void say_hello(string name){
    cout << name << "你好,我是黑马程序员" << endl;
}

int main() {

    say_hello("张三");
    say_hello("李四");
    return 0;
}

9.空参函数

函数的传入参数和返回值一样也是可选的,即声明不需要参数(形参)的传入(注意:()必须写)。

cpp 复制代码
void i_like_you()
{
    cout << "小美我喜欢你! << endl;
}

本节内容代码演示:

cpp 复制代码
#include <iostream>
using namespace std;

void i_like_you() {
    for (int i = 0; i < 5; i ++){
        cout << "小美我喜欢你" << endl;
}
}

int main() {
    i_like_you();
    return 0;
}

10.函数的嵌套调用

函数作为一个独立的代码单元,可以在函数内调用其他函数。这种嵌套调用关系没有任何限制,可以根据需要无限嵌套。以下面的函数为例,在函数fuc_a中又调用了func_b和func_c函数:

cpp 复制代码
int func_a(){
    
    code;
    int num = func_b();
    int result = num + func_c();
    code;

    return result;
}

本节内容代码演示:

cpp 复制代码
#include <iostream>
using namespace std;

/*
 * 喜欢小美,正在追求中,每天3种追求方案/;
 * 1.送早餐、送花、说喜欢
 * 2.送花、说喜欢、邀请一起看电影
 * 3. 邀请一起看电影、送花、说喜欢
 *
 * 用函数的思想模拟这些动作。
 */
void send_food() {
    cout << "小美,我给你买了早餐!" << endl;
}

void send_flower() {
    cout << "小美,我给你买了玫瑰花,你真好看!" << endl;
}

void say_love() {
    cout << "小美,我很喜欢你" << endl;
}

void watch_ovie() {
    cout << "小美,我们一起看电影吧" << endl;
}
void i_like_you(int num) {
    switch (num) {

        case 1:
            send_food();
            send_flower();
            say_love();
            break;
        case 2:
            send_flower();
            say_love();
            watch_ovie();
            break;
        case 3:
            watch_ovie();
            send_flower();
            say_love();
            break;
        default:
            cout << "今天不追求小美了,去打球去" << endl;
    }
}

int main() {

    cout << "今天天气不错,执行方案3追求小美" << endl;
    i_like_you(3);

    cout << "第二天天气也不错,执行方案2" << endl;
    i_like_you(2);
    
    return 0;

}

11.函数的嵌套调用练习题讲解

cpp 复制代码
#include <iostream>
using namespace std;

// 函数1接收2个int值传入,返回最小值
int get_min(int a, int b) {
    return a < b ? a : b;
}

// 函数2接收2个int值传入,返回最大值
int get_max(int a, int b) {
    return a > b ? a : b;
}

// 函数3接收2个int值传入,返回一个结构体
// 结构体有2个成员,成员1最小值,成员2最大值
struct MinAndMax {
    int min;
    int max;
};
struct MinAndMax get_min_and_max(int a, int b) {

    // 函数3调用另外2个函数并将其包装为结构体
    int min = get_min(a, b);
    int max = get_max(a, b);
    struct MinAndMax v = {min, max};

    return v;
}

int main() {

    struct MinAndMax v = get_min_and_max(3, 4);
    cout << "结构体最小值:" << v.min << endl;
    cout << "结构体最大值:" << v.max << endl;
    return 0;

}

12.参数的值传递和地址传递

之前学习的函数形参声明使用"值传递"的参数传入方式。例如下面代码的输出结果是x = 1

y = 2:

cpp 复制代码
void switch_num(int a, int b) {
    int temp;
    temp = a;
    a = b;
    b = temp;
}

int main() {

    int x = 1, y = 2;
    switch_num(x, y);
    cout << "x = " << x << endl;
    cout << "y = " << y << endl;
    return 0;

}

函数通过值传递接收参数时,系统会在内存栈中为该函数开辟一个独立的栈帧 ,并将传入的实参复制为局部变量。函数体中操作的只是这份副本,和主函数的变量没有地址联系。函数结束后栈帧销毁,因此不会对原变量产生任何修改效果。

另一种不传递值而改用传递指针/地址的写法如下,此时结果真正完成了交换:

cpp 复制代码
#include <iostream>
using namespace std;

void switch_num(int *a, int *b) {
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;
}

int main() {

    int x = 1, y = 2;
    switch_num(&x, &y);
    cout << "x = " << x << endl;
    cout << "y = " << y << endl;
    return 0;

}

本节内容代码演示:

cpp 复制代码
#include <iostream>
using namespace std;

void switch_num(int a, int b) {
    int temp;
    temp = a;
    a = b;
    b = temp;
}

void switch_num_pointer(int *a, int *b) {
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;
}

int main() {

    int a = 1, b = 2;
    switch_num(a, b);
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    int x = 1, y = 2;
    switch_num_pointer(&x, &y);
    cout << "x = " << x << endl;
    cout << "y = " << y << endl;
    return 0;

}

13.函数综合案例------黑马ATM

主菜单效果

查询余额效果

存取款效果

需求:

  • 需要记录银行卡余额(默认5000000)并记录其变化
  • 定义一个变量:name,用来记录客户姓名(启动程序时输入)
  • 定义如下函数:
    • 余额查询函数
    • 存款函数
    • 取款函数
    • 主菜单函数
  • 要求:
    • 程序启动后要输入客户姓名
    • 查询余额、存款、取款后都会返回主菜单
    • 存款、取款后,都应显示一下当前余额
    • 客户选择退出或输入错误,程序会退出,否则一直运行

代码:

cpp 复制代码
#include <iostream>
using namespace std;

/*
* 实现余额查询函数、存款函数、取款函数、主菜单函数,共4个函数
*/

// 1.查询函数
void query_balance(const string * const name, int * const money) {  // 限制money指针的指向,但是不限制值的修改
    // 当前案例参数可以使用值传递,但是性能略低,因为涉及到值的复制
    // 同时四个函数都需要使用到这个变量,所以使用引用传递去取同一块内存区域更合理
    cout << "------------查询余额------------" << endl;
    cout << *name << ",您好,您的余额剩余:" << *money << "元" << endl;
}

// 2.存款函数
void deposit_money(const string * const name, int * const money, int num) {
    // 存款数额不需要使用引用传递,该变量为临时变量,不需要传递
    cout << "------------存款------------" << endl;
    cout << *name << ",您好,您存入" << num << "元成功" << endl;
    // 余额发生变更
    *money += num;
    cout << *name << ",您好,您的余额剩余:" << *money << "元" << endl;
}

// 3.取款函数
void withdraw_money(const string * const name, int * const money, int num) {
    cout << "------------取款------------" << endl;
    cout << *name << ",您好,您取出" << num << "元成功" << endl;
    // 余额发生变更
    *money -= num;
    cout << *name << ",您好,您的余额剩余:" << *money << "元" << endl;
}

// 4.主菜单函数
int menu(const string * const name) {  // name只查询不修改,所以使用const修饰限定
    cout << "------------主菜单------------" << endl;
    cout << "您好,欢迎来到黑马ATM,请选择操作:" << endl;
    cout << "1.查询余额" << endl;
    cout << "2.存款" << endl;
    cout << "3.取款" << endl;
    cout << "4.退出" << endl;
    int num;
    cout << "请输入您的选择:";
    cin >> num;
    return num;
}

int main() {

    // 要求输入用户姓名
    string name;
    cout << "请输入您的用户名:";
    cin >> name;

    int * money = new int;
    * money = 500000;   // 余额默认500000元
    // 保证执行完所有操作都返回主菜单,退出除外
    bool is_run = true;
    while (is_run) {
        // 显示主菜单
        int select_num = menu(&name);
        // 根据选择执行相应操作
        switch (select_num) {
            case 1:
                query_balance(&name, money);
                break;
            case 2:
                int num_for_deposit;
                cout << "请输入存款金额:";
                cin >> num_for_deposit;
                deposit_money(&name, money, num_for_deposit);
                break;
            case 3:
                int num_for_withdraw;
                cout << "请输入取款金额:";
                cin >> num_for_withdraw;
                withdraw_money(&name, money, num_for_withdraw);
                break;
            default:
                cout << "您选择了退出,程序退出" << endl;
                is_run = false;

        }
    }
    return 0;
}

运行结果:

cpp 复制代码
请输入您的用户名:may
------------主菜单------------
您好,欢迎来到黑马ATM,请选择操作:
1.查询余额
2.存款
3.取款
4.退出
请输入您的选择:1
------------查询余额------------
may,您好,您的余额剩余:500000元
------------主菜单------------
您好,欢迎来到黑马ATM,请选择操作:
1.查询余额
2.存款
3.取款
4.退出
请输入您的选择:2
请输入存款金额:300000
------------存款------------
may,您好,您存入300000元成功
may,您好,您的余额剩余:800000元
------------主菜单------------
您好,欢迎来到黑马ATM,请选择操作:
1.查询余额
2.存款
3.取款
4.退出
请输入您的选择:4
您选择了退出,程序退出

14.函数传入数组

由于数组对象本身只是第一个元素的地址,所以数组传参不区分传值还是传地址,其本质都是传递指针(地址) 。也就是说下面三种代码虽然写法不一样,但是在本质上是一样的。

cpp 复制代码
void func1(int arr[]) {

}

void func2(int arr[10]) {

}

void func3(int * arr) {
    
}

因此,如果在传参中需要传入数组,通常会附带第二个参数,也就是数组的长度,因为c+不会检查数组的内存边界。本节内容代码演示:

cpp 复制代码
#include <iostream>
using namespace std;

void func1(int arr[]) {
    cout << "函数内统计的数组总大小: " << sizeof(arr) << endl;

}

void func2(int arr[10]) {

}
// 正常写法
void func(int arr[], int length) {
    for (int i = 0; i < length; i++) {
        cout << arr[i] << endl;
    }

}
void func3(int * arr) {

}

int main() {
    int arr[] = {1,2,3,4,5};
    cout << "在main函数内统计的数组总大小: " << sizeof(arr) << endl;

    func1(arr); // 输出总是8,原因是sizeof(arr)计算的是指针的大小
    // 因此传递数组就相当于传递指针,此时无法用sizeof计算数组大小

    func(arr,5); // 输出为10,因为sizeof(arr)计算的是数组的大小
}

15.函数传入数组练习题讲解------数组排序函数

思路:使用内外两层循环,设置2个额外变量,记录当前的最小值及其下标,用于每一次内层循环完毕后将最小值移动到当前外层循环的起始坐标。结束条件:外层循环至倒数第二个元素。代码实现:

cpp 复制代码
#include <iostream>
using namespace std;

void sort_array(int *arr, int length) {
    int min,min_index;
    for (int i = 0; i < length - 1; i++) {
        for (int j = i; j < length; j++) {
            // 第一个元素直接放入min和记录min_index
            if (i == j) {
                min = arr[i];
                min_index = i;
            }
            // 非本次内循环第一个元素就要比较大小
            if (arr[j] < min) {
                min = arr[j];
                min_index = j;
            }
        }
        // 本次内循环完成后要进行一次转换,将最小值与当前内循环起始位置元素交换
        if (min_index != i) {
            int temp = arr[i];
            arr[i] = min;
            arr[min_index] = temp;
        }
    }
}

int main() {

    int arr[] = {1, 3, 5, 7, 9, 2, 4, 6, 8, 0};
    sort_array(arr, 10);
    for (int i = 0; i < 10; i++) {
        cout << arr[i] << " ";
    }

}

16.引用的基本概念

函数的参数传递除了值传递和地址传递外,还有引用传递。引用是变量的别名,对变量进行操作和对引用进行操作是一样的。

区别引用和指针

引用并不是指针,引用不可修改、必须初始化、不能为空,而指针恰好相反。

对变量A创建一个引用,相当于给它起了一个别名B,B的内存区域就锁定了。同时,因为不可修改,所以在创建时必须初始化。因为必须初始化,所以引用不可为空。

引用语法:

cpp 复制代码
数据类型& 引用名 = 被引用变量;

int a = 10;
int& b = a;

double d1 = 11.11;
double& d2 = d1;

本节内容代码演示:

cpp 复制代码
#include <iostream>
using namespace std;


int main() {

    int a = 20;
    int& b = a;
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    b = 30;
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;

    double c = 20.2;
    double& d = c;
    cout << "c = " << c << endl;
    cout << "d = " << d << endl;
    d = 30.1;
    cout << "c = " << c << endl;
    cout << "d = " << d << endl;

    return 0;

}

17.引用传参

引用传参是最常见的传参方式,下面是已经学过的三种传参方式的总结:

  1. 值传递:会复制值,无法对实参本身产生影响。
  2. 地址传递:会复制地址,对实参本身可以产生影响,指针写法较麻烦。
  3. 引用传递:既可以像普通变量一样操作内容,又可以对参数本身产生影响。

引用传递写法(接收引用对象):

cpp 复制代码
void switch_num(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

本节内容代码演示(与指针传参对比):

cpp 复制代码
#include <iostream>
using namespace std;


void switch_num1(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}
void switch_num2(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}
int main() {

    int a = 10, b = 20;
    switch_num1(a, b);
    cout << a << " " << b << endl;
    switch_num2(&a, &b);
    cout << a << " " << b << endl;

    return 0;

}

18.函数返回指针及局部变量

函数的返回值可以是指针(内存地址),语法如下:

cpp 复制代码
返回值类型 * 函数名(形参列表){

    函数体;
    
    return 指针;
}

实际代码举例:

cpp 复制代码
int *add(int a, int b)
{
    int sum;
    sum = a + b;
    return &sum;
}

注意,当前代码的格式(语法)正确,但是无法正常使用,原因是sum在函数内声明,属于局部变量,作用范围仅在函数内部,因此函数执行完成后就会销毁sum的内存区域。

如何规避?------动态内存管理

代码演示:

1.错误写法(静态分配内存),add函数执行完后就被回收了,因此返回result指针的地址是悬垂指针,不能安全使用。

cpp 复制代码
#include <iostream>
using namespace std;

// 返回指针的函数,就在函数返回值声明和函数名之间加上*即可
int *add(int a, int b)
{
    int sum;
    sum = a + b;
    return &sum;    // 直接return sum的话,本质返回的是一个int类型的变量,所以要加上取地址符&
}

int main() {

    int *result = add(1, 2);
    cout << *result << endl;
    return 0;

}

执行结果:

cpp 复制代码
进程已结束,退出代码为 -1073741819 (0xC0000005)

2.正确在函数内返回指针的写法,函数内动态分配内存+main函数调用函数执行完毕后手动销毁:

cpp 复制代码
#include <iostream>
using namespace std;

// 返回指针的函数,就在函数返回值声明和函数名之间加上*即可
int *add(int a, int b)
{
    int *sum = new int;
    *sum = a + b;
    return sum;    // 直接return sum的话,本质返回的是一个int类型的变量,所以要加上取地址符&
}

int main() {

    int *result = add(1, 2);
    cout << *result << endl;

    delete result;
    return 0;

}

19.static关键字

static表示静态(将内容存入静态内存区域,后续学习),可以修饰变量和函数。

当static修饰变量,比如函数内的局部变量,可以延长生命周期整个程序运行周期

语法(在变量类型前加入static关键字即可):

cpp 复制代码
int *add(int a, int b)
{
    static int *sum = new int;
    *sum = a + b;
    return sum;    
}

此时sum变量仅被初始化一次(在函数第一次调用时),并且持续存在(不因函数运行结束而销毁),直至程序结束。

本节内容代码演示:

cpp 复制代码
#include <iostream>
using namespace std;

int *add(int a, int b)
{
    static int sum ;
    sum = a + b;
    return &sum;
}

int main() {

    int *result = add(1, 2);
    cout << *result << endl;

    return 0;
}

static还可以修饰函数等,其余的作用会在后面讲到。

使用static关键字还是动态内存分配?

分情况。如果变量是一个储存几万个数据的数组,为了不占用内存建议手动分配内存并释放;如果数据很小且内存充足可以使用static关键字。

20.函数返回数组

我们已经学过函数返回指针,由于数组对象本身是第一个数组元素的地址,所以返回数组本质上就是返回指针。

函数返回数组的语法要求按照返回指针的方式声明返回值

cpp 复制代码
int * func()
{
    ···
    return arr;
}

要注意,返回的数组不可是局部变量(生命周期仅限函数),可以返回全局数组static修饰的数组。常见的三种返回方式:

cpp 复制代码
int * func()
{
    static int arr[] = {1, 2, 3};
    return arr;
}

int * func()
{
    int * arr = new int[3]{1, 2, 3};
    return arr;
}

int arr[3] = {1, 2, 3};
int * func()
{
    ···
    return arr;
}

本节内容代码演示:

cpp 复制代码
#include <iostream>
using namespace std;

int * func1()
{
    static int arr[] = {1, 2, 3};
    return arr;
}

int * func2()
{
    int * arr = new int[3]{1, 2, 3};
    return arr;
}

// 可以用但不推荐,原因是该数组函数只使用一次但是设置成了全局变量,把局部逻辑依赖写死到全局变量上
int arr[3] = {1, 2, 3};
int * func3()
{
    return arr;
}

int main() {

    int * p1 = func1();
    for (int i = 0; i < 3; i++) {
        cout << p1[i] << ' ' ;
    }
    cout << endl;

    int * p2 = func2();
    for (int i = 0; i < 3; i++) {
        cout << p2[i] << ' ';
    }
    cout << endl;
    delete[] p2;

    int * p3 = func3();
    for (int i = 0; i < 3; i++) {
        cout << p3[i] << ' ';
    }
    cout << endl;
    return 0;
}

提示:尽管三种函数返回数组都能运行,但是不推荐使用这种行为,原因是如果忘记delete释放内存十分危险,而static全局变量则要一直占用内存。因此,最推荐的方式是:在函数外部将数组搞定,然后通过传参的形式把数组传入函数(传地址、传引用)。

21.函数返回数组的改进

上节提到不推荐函数返回数组,如果真的需要在函数里面操作数组,推荐的操作步骤如下:

  1. 在函数声明时接收数组传入
  2. 调用函数前自行创建好数组
  3. 把数组的指针传给函数

示例:

cpp 复制代码
#include <iostream>
using namespace std;

void plus_one_in_arr(int * arr, const int length) {
    for (int i = 0; i < length; i++) {
        arr[i] = i + 1;
    }
}

int main() {
    int arr[10];
    plus_one_in_arr(arr, 10);
    for (int i = 0; i < 10; i++) {
        cout << arr[i] << endl;
    }
    return 0;
}

22.函数的默认值

函数在定义时可以指定默认值,也就是说调用函数时不传入实参,使用函数默认值代替,例如:

cpp 复制代码
#include <iostream>
using namespace std;

void say_hello(const string msg="chloe") {
    cout << "Hi I am " << msg << endl;
}

int main() {

    say_hello();
    say_hello("hana");
    return 0;
}

输出:

cpp 复制代码
Hi I am chloe
Hi I am hana

注意:一个常见的错误写法是,提供默认值的参数右侧的参数未提供默认值。例如:

cpp 复制代码
void add(int x = 3, int y)
void add(int x, int y = 6, int z)

以下为正确写法:

cpp 复制代码
void add(int x = 3, int y = 6, int z = 9)
void add(int x, int y = 6, int z = 9)
void add(int x, int y, int z = 9)

本节内容代码演示:

cpp 复制代码
#include <iostream>
using namespace std;

void say_hello(const string msg="chloe") {
    cout << "Hi I am " << msg << endl;
}

int add(int x, int y, int z = 10) {
    return x + y + z;
}
int main() {

    say_hello();
    say_hello("hana");
    cout << "1 + 2 + 10 = " << add(1,2) << endl;
    cout << "1 + 2 + 3 = " << add(1,2,3) << endl;
    return 0;
}

23.【了解】手动编译的流程和include指令

代码经过编译转变成可执行程序exe文件,其中编译又会经历四个过程:

  1. 预处理:指的是将源代码中的预处理指令,如宏展开(比如#define NUM 10)、条件编译指令(比如#ifdef、#ifndef)、包含头文件(比如#include<iostream>)等处理掉,预处理命令是 g++ -E xxx.cpp -o xxx.i
  2. 编译:将预处理后的代码转换为汇编语言,编译命令是 g++ -S xxx.i -o xxx.s
  3. 汇编:将汇编代码转换为二进制机器码,汇编命令是 g++ -c xxx.s -o xxx.o
  4. 链接:将机器码文件和所需的其他库链接得到程序,编译的命令是 g++ xxx.o -o xxx.exe

在clion集成开发环境中演示g++手动编译流程:

1.新建一个文件夹"手动编译演示"

2.切换成命令行模式

点击左下方的终端图标进入命令行,输入" cd .\xxx"(xxx是指当前cpp文件所在的文件夹),点击回车就完成了切换目录。

3.输入预处理指令:

cpp 复制代码
g++ -E .\手动演示.cpp -o 演示.i            

可以看到当前目录里生成了新的文件演示.i,文件长达3万行。(补充:在源文件界面按住ctrl键就可以点进去查看iostream头文件,可以看到该文件有80多行,但是包含其他的头文件和宏展开、条件编译处理如

这些在预处理阶段全部都要替换成对应的源代码,从而生成一个中间文件(如 .i 文件),供编译器后续分析和翻译。

4.输入编译指令:

cpp 复制代码
g++ -S .\演示.i -o 汇编.s

这一步将中间文件变成汇编文件,可以看到命令执行完毕后当前文件目录下多出一个汇编.s的文件。

5.输入汇编指令:

cpp 复制代码
 g++ -c .\汇编.s -o 机器码.o

这一步将汇编文本转变成(二进制)机器文件,可以看到命令执行完毕后当前文件目录下多出一个机器码.o文件。

注意:中间文件、汇编代码和机器码文件都不是标准程序!

6.输入链接指令:

cpp 复制代码
g++ .\机器码.o -o 演示程序.exe

这是最后一把,完成这一步后,我们在命令行运行.exe文件:

cpp 复制代码
 .\演示程序.exe

就可以看到输出:

至此,手动编译就是成功了。

简易版手动编译:

有同学问,这种方法太繁琐了记不住,并且我还不想用图形化界面的三角执行键,有没有不那么吃操作的方法呢?有的有的,我们可以在命令行输入

cpp 复制代码
g++ .\手动编译演示.cpp -o 程序.exe

就可以运行了:

甚至还有更简单的,直接输入

cpp 复制代码
g++ 手动编译演示.cpp 

(由于没有设置输出文件的名称,得到了默认文件名a.exe),就可以在文件目录下看到可执行文件了。

24.【了解】多文件函数编程

在开发大型C++项目时不会将全部代码写在一个文件中,我们在构架项目的时候可以将函数定义到其他文件中,以确保项目组织更加清晰。

在c++中一般有2类文件:

  • .h文件(头文件,c语言也有)
  • .cpp文件(源代码文件)

我们可以将

  • 函数的声明写在.h文件中
  • 函数的实现写在同名的.cpp文件中

代码演示:

创建一个新的文件夹起名为"多文件函数编程",新建一个main.cpp文件,在该文件中按照以往的编程方式,先声明2个函数,再在下面写上它们的具体实现(类似变量的先声明后实现):

cpp 复制代码
#include <iostream>
using namespace std;
// 声明
void add(int x, int y);
void add2(int x);

// 实现
void add(int x, int y) {
    cout << x + y << endl;
}

void add2(int x) {
    cout << x + 10 << endl;
}

int main() {

    add(10, 20);
    add2(10);
    return 0;
}

然后新建一个my_func源文件,将函数声明部分和实现部分移动到该文件(头文件也要补充上),此时,缺少main文件和函数文件的链接,需要在main文件引入自定义库函数:

cpp 复制代码
#include <iostream>
#include "my_func.cpp"

using namespace std;

int main() {

    add(10, 20);
    add2(10);
    return 0;
}

接下来需要编译,我们选择使用命令行编译,先使用cd命令切换到该文件目录下,然后输入

cpp 复制代码
g++ .\main.cpp

就可以看到输出结果:

30

20

到此为止就实现了多文件编程,但是不够规范,没有用到头文件。

在当前文件夹下新建一个头文件,起名为my_func(与my_func.cpp)同名,然后将声明放进该头文件中,my_func.cpp文件中只保留了实现:

cpp 复制代码
//
// Created by Hippocrates on 25-5-25.
//

#ifndef MY_FUNC_H
#define MY_FUNC_H

// 声明
void add(int x, int y);
void add2(int x);


#endif //MY_FUNC_H

通过该头文件,实现了函数的声明和实现的分离,在以后的大型工作项目中便于管理和使用。

为了让my_func.cpp文件和my_func.h文件联系起来,在my_func.cpp文件中引入该头文件:

cpp 复制代码
#include "my_func.h"

再在命令行输入编译指令"g++ .\main.cpp"就可以了。

提示:如果将mian函数中的"#include "my_func.cpp" 改成"#include "my_func.h",直接输入上面的编译指令会报错,因为my_func头文件中没有提供函数的实现,编译器无法使用。正确的做法是将编译指令改为"g++ .\main.cpp .\my_func.cpp",此时便可正常编译了。

相关推荐
大信团队—跨境TRO3 小时前
[25-cv-05718]BSF律所代理潮流品牌KAWS公仔(商标+版权)
笔记·跨境电商·亚马逊·版权·侵权维权
种花生的图图4 小时前
《DeepDeSRT:基于深度学习的文档图像表格检测和结构识别》学习笔记
人工智能·笔记·深度学习·学习
序属秋秋秋4 小时前
《数据结构初阶》【番外篇:快速排序的前世今生】
c语言·数据结构·c++·笔记·leetcode·排序算法
无名咸鱼4 小时前
企业微信内部网页开发流程笔记
笔记·企业微信
一点.点12 小时前
李沐动手深度学习(pycharm中运行笔记)——10.多层感知机+从零实现+简介实现
人工智能·笔记·python·深度学习·pycharm
moxiaoran575314 小时前
Python学习笔记--Django 表单处理
笔记·python·学习
路过的一个普通人15 小时前
C 语言学习笔记二
c语言·笔记·学习
LearnTech_12316 小时前
【学习笔记】GitLab 下载安装与配置
笔记·学习·gitlab
枷锁—sha17 小时前
【HW系列】—web常规漏洞(CSRF与SSRF)
服务器·前端·网络·笔记·安全·网络安全·csrf
IT199517 小时前
React笔记-Ant Design X样本间对接智谱AI
前端·笔记·react.js