C++基础从0到1入门编程(二)

系统学习C++

方便自己日后复习,错误的地方希望积极指正

往期文章:C++基础从0到1入门编程(一)

参考视频:

1.黑马程序员匠心之作|C++教程从0到1入门编程,学习编程不再难

2.系统化学习C++

1 函数指针和回调函数

如果把函数的地址作为参数传递给函数,就可以在函数中灵活的调用其他函数

三步走:

(1)声名函数指针

cpp 复制代码
int fun(int a, string b);
//函数指针的声明
int (*pfun)(int, string);

(2)让函数指针指向函数的地址

cpp 复制代码
pfun = fun;

(3)通过函数指针调用函数

cpp 复制代码
(*pfun)(2,"asd"); // C
pfun(2,"asd");    // C++
cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

void func(int no, string str)
{
    cout << no << ' ' << str;
}
int main()
{
    int bh = 3;
    string message = "i'm a dog";
    func(bh, message);
    // 声明函数指针
    void (*pfunc)(int, string);
    // 对函数指针赋值,函数指针名 = 函数名
    pfunc = func;
    // 用函数指针名调用函数 C++
    pfunc(bh, message);
    // 用函数指针名调用函数 C语言
    (*pfunc)(bh, message);
    return 0;
}

函数指针的使用场景(回调函数):

开了一个皮包公司,专门承接表白业务

表白之前的准备工作和表白之后的收尾工作,公司都帮你做好,向女生表白这件事由你自己来做,别人是帮不了的。

用函数指针回调函数和用函数名调用函数意义不一样

回调函数:把一个函数的代码嵌入到另一个函数中,调用者函数提供了主体的流程和框架,具体的功能由回调函数来实现。写调用者函数的时候,只确定回调函数的种类,不关心回调函数的功能。

函数名调用函数:调用函数时,知道函数名和功能

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

void BigDavid(int a)
{
    cout << a << "Hello World" << endl;
}

void Hw(int a)
{
    cout << a << "asd\n";
}
//void show(void (*pf)())
//{
//    cout << "front" << endl;
//    pf();
//    cout << "done" << endl;
//}

void show(void (*pf)(int), int b)
{
    cout << "front" << endl;
    pf(b);
    cout << "done" << endl;
}

int main()
{
    show(BigDavid, 3);
    show(Hw, 4);
}

2 补充一些数组的概念

2.1 清空数组和复制数组

(1)清空数组

memset()函数可以把数组中全部元素清零(适用于C++基本数据类型)
void *memset(void *s, int c, size_t n);

在Linux下,使用memcpy()函数要加头文件#include <cstring>

(2)复制数组

用memcpy()函数可以将数组中全部的元素复制到另一个相同大小的数组
void *memcpy(void *dest, const void *src, size_t n);

第一个参数:目标数组名

第二个参数:原数组名

第三个参数:整个数组占用内存空间的大小

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

int main()
{
    int bh[] = {1, 2, 4, 6};
    string name[3];

    for (int i = 0; i < 4; i++)
    {
        cout << i << ' ' << bh[i] << endl;
    }
    memset(bh,0,sizeof(bh));
    for (int i = 0; i < 4; i++)
    {
        cout << i << ' ' << bh[i] << endl;
    }

    int bh1[sizeof(bh) / sizeof(int)];
    memcpy(bh1, bh, sizeof(bh));
    for (int i = 0; i < 4; i++)
    {
        cout << i << ' ' << bh1[i] << endl;
    }

}
2.2 一维数组用于函数的参数

1.指针的数组表示

数组名[下标] = *(数组首地址 + 下标)

地址[下标] = *(地址 + 下标)

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

int main()
{
    char a[20];
    int* p = (int *)a;
    for (int i = 0; i < 5; i++)
    {
        p[i] = i + 300;
    }
    for (int i = 0; i < 5; i++)
    {
        cout << *(p + i) << endl;
    }
}

2.一维数组用于函数的参数
void fun(int* arr, int len);
void fun(int arr[], int len);

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

void func(int arr[], int len)
{
    for (int i = 0; i < len; i++)
    {
        cout << arr[i] << endl;
    }
}
int main()
{
    int a[] = {1, 2, 4, 5};
    func(a, 4);
}
2.3 用new动态创建一维数组

数据类型 *指针 = new 数据类型[数组长度];

释放一维数组的语法:delete[] 指针;

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

int main()
{
    int* arr = new int[8];
    for (int i = 0; i < 8; i++)
    {
        arr[i] = 100 + i;
        cout << *(arr + i) << endl;
    }
    delete[] arr;
    return 0;
}

Tip:

(1)动态创建的数组没有数组名,不能用sizeof运算符

(2)可以用数组表示法和指针表示法使用动态创建的数组

(3)必须使用delete[]释放内存

(4)不要用delete[]释放不是new[]分配的内存

(5)不用delete[]释放同一个内存块两次(否则相同于操作野指针)

(6)对空指针用delete[]是安全的(释放内存后,应该把指针置空nullptr)

(7)声名普通数组,数组长度可以用变量,相当于在栈上动态创建数组,并且不需要手动释放

(8)非常重要:如果内存不足,调用new会产生异常,导致程序终止;如果在new关键字后面加(std::nothrow)选项,则返回nullptr,不会产生异常

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

int main()
{
    int *a = new (std::nothrow) int[10000000001];
    if (a == nullptr)
    {
        cout << "failed" << endl;
    }
    else
    {
        a[10000000000] = 8;
        delete[] a;
    }
    return 0;
}

(9)为什么用delete[]释放数组的时候,不需要指定数组的大小?因为系统会自动跟踪已分配数组的内存

2.4 数组最经典的两种应用场景:排序和查找

(1)数组的排序qsort
qsort()函数用于对各种数据类型的数组进行排序
void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void*, const void *))

第一个参数:数组的起始地址

第二个参数:数组元素的个数

第三个参数:数组元素的大小(sizeof(数组的数据类型))

第四个参数:回调函数的地址

回调函数决定了排序的顺序
int compar(const void* p1, const void* p2);

1)如果函数的返回值< 0 ,那么p1所指向元素会被排在p2所指向元素的前面。

2)如果函数的返回值==0,那么p1所指向元素与p2所指向元素的顺序不确定。

3)如果函数的返回值> 0 ,那么p1所指向元素会被排在p2所指向元素的后面。

qsort()函数细节

(1)形参中的地址用void是为了支持任意数据类型,在回调函数中必须具体化

(2)排序的需求除了升序和降序,还有很多不可预知的情况,只能用回调函数

cpp 复制代码
#include <iostream>
using namespace std;
// 从小到大排序
int compasc(const void* p1, const void* p2) // 升序的回调函数
{
//    if (*(int*)p1 < *(int*)p2) return -1;
//    if (*(int*)p1 == *(int*)p2) return 0;
//    if (*(int*)p1 > *(int*)p2) return 1;
    return *(int*)p1 - *(int*)p2;
}
int compdesc(const void* p1, const void* p2) // 降序的回调函数
{
    return *(int*)p2 - *(int*)p1;
}
int main()
{
    int a[8] = { 2,1,7,8,4,0, 11, 9};
    for (int i = 0; i < 8; i++)
    {
        cout << a[i] << ' ';
    }
    cout << endl;
    qsort(a,sizeof(a)/sizeof(a[0]),sizeof(a[0]),compdesc);
    for (int i = 0; i < 8; i++)
    {
        cout << a[i] << ' ';
    }
    return 0;
}

(2)数组的查找

二分法详见:数组之二分查找

2.5 C风格字符串

C++用的是string,能自动扩展,不考虑内存和野指针问题

string是C++的类,封装了C风格的字符串

在某些场景,C风格字符串更方便、高效

C语言约定:如果字符型(char)数组的末尾包含了空字符\0(也就是0),那么该数组中的内容就是一个字符串

cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
    string x = "XY";
    cout << x[0] << x[1] << x[2];
}

一些操作

(1)初始化

cpp 复制代码
char name[11];
char name[11] = { "hello" };
char name[] = { "sd" };
char name[11] { "hello" };
char name[11] = { 0 }; //把全部元素初始化为0

(2)清空字符串

cpp 复制代码
memset(name, 0, sizeof(name));
name[0] = 0; //不推荐

(3)字符串复制strcpy()

复制完字符串后,会在dest后追加0

如果参数dest所指的内存空间不够大,会导致数组的越界

cpp 复制代码
strcpy(name1, name);

将参数name字符串拷贝至参数name1所指的地址

(4)字符串赋值strncpy

cpp 复制代码
strncpy(name1, name, 3);

把name前3个字符的内容复制到name1中

如果src字符串长度小于n,则拷贝完字符串后,在dest后追加0,直到n个。

如果src的长度大于等于n,就截取src的前n个字符,不会在dest后追加0。

如果参数dest所指的内存空间不够大,会导致数组的越界。

(5)获取字符串的长度strlen()

(6)字符串拼接strcat()

(7)字符串拼接strncat()

(8)字符串比较strcmp()和strncmp()

相等返回0,str1大于str2返回1,str1小于str2返回-1

cpp 复制代码
strcmp(str1, str2);
strcmp(str1, str2, 3);

(9)查找strchr()和strchr()
const char *strchr(const char *s, int c);

返回在字符串s中第一次出现c的位置,如果找不到,返回0。
const char *strrchr(const char *s, int c);

返回在字符串s中最后一次出现c的位置,如果找不到,返回0。

(10)查找字符串strstr()
char * strstr(const char* str,const char* substr);

功能:检索子串在字符串中首次出现的位置。

返回值:返回字符串str中第一次出现子串substr的地址;如果没有检索到子串,则返回0。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>
using namespace std;
int main()
{
    // 初始化方法
    char name[11]; // 存放了10个字符,没有初始化,里面都是垃圾值
    char name1[11] = "hello";
    char name2[] = { "asd" }; //4
    char name3[11] = { "asd" };
    char name4[11] {"asd"}; // C++11标准
    char name5[11] = {0};

    for (int i = 0; i < 11; i++)
    {
        cout << name1[i] << endl;
    }
    // 清空字符串
    memset(name1, 0, sizeof(name));

    // 字符串复制或赋值strcpy()
    strcpy(name, name3);

    // 字符串赋值或赋值strncpy()
    strncpy(name, "hello", 8);

    // 获取字符串的长度strlen()
    cout << strlen(name) << endl;

    // 字符串拼接strcat()
    cout << strcat(name, name3) << endl;

    // 字符串拼接strncat()
    cout << strncat(name, name3, 2) << endl;

    // 字符串比较strcmp()和strncmp()
    cout << strcmp(name1, name2) << endl;
    cout << strncmp(name1, name2, 2) << endl;

    // 字符查找strchr()和strtchr()
    cout << strchr(name1, 0) << endl;
    // 字符串查找strstr()
    cout << strstr(name1, name2) << endl;
}

(11)Tip

1.结尾标志0后面都是垃圾内容

2.字符串每次使用前都要初始化

3.不要在子函数中对字符指针用sizeof运算,所以,不能在子函数中对传入的字符串进行初始化,除非字符串的长度也作为参数传入到了子函数中。

2.6 二维数组一些补充

其他一些基本概念在C++基础从0到1入门编程(一)

(1)清空二维数组

用memset()函数把二维数组中全部的元素清零

(2)复制二维数组memcpy()

(3)二维数组用于函数的参数

复习之前学过的指针

cpp 复制代码
int* p;            // 整型指针
int* p[3];         // 一维整型指针数组
int* p();          // 函数p的返回值类型是整型的地址
int (*p)(int, int);// p是函数指针,函数的返回值是整型

行指针

数据类型 (*行指针名)[行的大小];

cpp 复制代码
int (*p1)[3];  // p1是行指针,用于指向数组长度为3的int型数组

二维数组名是行地址

cpp 复制代码
int bh[2][3] = { {1, 2, 3}, {2, 1, 4} };

bh是二维数组名,该数组有两元素,每一个元素本身又是一个数组长度为3的整型数组。

bh:数组长度为3的整型数组类型的行地址

如果存放bh的值,要用数组长度为3的整型数组类型的行指针

cpp 复制代码
int bh[2][3] = { {1, 2, 3}, {2, 1, 4} };
int (*p)[3] = bh;

int bh[4][2][3];
int (*p)[2][3] = bh;

把二维数组传递给函数
void fun(int (*p)[3], int len);
void fun(int p[][3], int len);

实操:

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

//void fun(int p[][3], int len)
//{
//    for (int i = 0; i < len; i++)
//    {
//        for (int j = 0; j < 3; j++)
//        {
//            cout << p[i][j] << " ";
//        }
//        cout << endl;
//    }
//}
void fun(int (*p)[2][3])
{
    int i = 0;
    for (int a = 0; a < 4; a++)
    {
        for (int b = 0; b < 2; b++)
        {
            for (int c = 0; c < 3; c++)
            {
                p[a][b][c] = i++;
            }
        }
    }
}
int main()
{
    int a[10];
    cout << a + 1<< endl;  // +4
    cout << &a + 1<< endl; // +40

    // int bh[2][3] = { {1, 2, 3}, {2, 1, 4} };
    // fun(bh, 2);
    int bh[4][2][3];
    memset(bh, 0, sizeof(bh));
    fun(bh);
    for (int a = 0; a < 4; a++)
    {
        for (int b = 0; b < 2; b++)
        {
            for (int c = 0; c < 3; c++)
                cout << bh[a][b][c] << '\t';
            cout << endl;
        }
        cout << endl << endl;
    }
}

3 结构体基本概念

3.1 定义结构体

结构体:将多种数据整合到一起,描述一个完整的对象

Tip:

(1)结构体成员可以用C++的类(string),但是不提倡

(2)C++中,结构体可以有函数,不推荐

(3)C++中,定义结构体可以指定缺省值

3.2 创建结构体变量

struct 结构体名 结构体变量名;
struct 结构体名 结构体变量名={成员一的值, 成员二的值,..., 成员n的值}

如果大括号内未包含任何东西或只写一个0,全部的成员都将被设置为0

Tip: C++中,struct关键字可以不写;可以在定义结构体的时候创建结构体变量

3.3 使用结构体

结构体变量名.结构体成员名;

3.4 占用内存的大小

使用sizeof运算符可以得到整个结构体占用内存的大小

整个结构体占用内存的大小不一定等于全部成员占用内存之和

3.5 清空结构体

用memset()函数可以把结构体中全部的成员清零。

bzero()函数也可以。

3.6 复制结构体

使用memcpy()函数把结构体中的全部元素复制到另一个相同类型的结构体,也可以直接用=

实操:

cpp 复制代码
#include <iostream>
#include <cstring>
using namespace std;
#pragma pack(8)

struct girl
{
    char name[20];
    int age;
    double weight;
    char sex;
    bool yanzhi;
};

int main()
{
    girl girl{"DiaoChan", 26, 99.9, 'X', true};
    cout << sizeof(girl) << endl;
    memset(&girl, 0, sizeof(girl));
    cout << girl.name << ' ' << girl.age << ' ' << girl.weight << ' ' << girl.sex << ' ' << girl.yanzhi << endl;
    return 0;
}

4 结构体指针

C++中,用不同类型的指针存放不同类型变量的地址,这一规则也适用于结构体

4.1 语法
cpp 复制代码
struct Girl girl;
struct Girl *pst = &girl; // 声明结构体指针,指向结构体变量

通过结构体指针访问结构体成员

cpp 复制代码
(*指针名).成员变量名
指针名->成员变量名 // 通常用这个,直观
4.2 用于函数的参数

如果把结构体传递给函数,实参取结构体变量的地址,函数的形参用结构体指针
如果不希望在函数中修改结构体变量的值,可以对形参加const约束

4.3 用结构体指针指向动态分配的内存的地址

结构体指针指向动态分配的内存的地址

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS // 如果要使用C标准库的字符串函数,需要加上这一行代码
#include <iostream>
#include <cstring>
using namespace std;

struct Girl
{
    char name[20];
    int age;
    double weight;
    char sex;
    bool yanzhi;
};

void fun(const Girl* pst)
{
    cout << pst->name << ' ' << pst->age << ' ' << pst->sex << ' ' << pst->weight << ' ' << pst->yanzhi;
}
int main()
{
    Girl* girl = new Girl({"DiaoChan", 26, 99.9, 'X', true});
    fun(girl);
    delete girl;
    return 0;
}

5 结构体数组

struct 结构体类型 数组名[数组长度];

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS // 如果要使用C标准库的字符串函数,需要加上这一行代码
#include <iostream>
#include <cstring>
using namespace std;

struct Girl
{
    char name[20];
    int age;
    double weight;
    char sex;
    bool yanzhi;
};

int main()
{
    Girl girls[3];
    memset(girls, 0, sizeof(girls));
    girls[1] = {"xigua",2,10.2,'X',false};
    strcpy(girls[0].name, "asd");girls[0].age = 11; (girls + 0)->yanzhi = true;
    for (int i = 0; i < 3; i++)
    {
        cout << (girls + i)->name << ' ' << girls[i].age << endl;
    }
    return 0;
}

6 结构体嵌入数组和结构体

多维数组用于函数的参数

做法:把二维或者多维数组放在结构体中,作为结构体的一个成员,调用函数的时候,把结构体的地址传给函数,函数的形参接受结构体的地址,这样就可以绕过二维和多维数据传指针的问题。

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

struct st_girl
{
    char name[21];
    int score[2][3] = {1,3,4,2,2,1};
    int age;
    double weight;
    char sex;
    bool yz;
};

void fun(st_girl* pst)
{
    for (int a = 0; a < 2; a++)
    {
        for (int b = 0; b < 3; b++)
        {
            cout << pst->score[a][b] << ' ';
        }
        cout << endl;
    }
}
int main()
{
    st_girl girl;
    fun(&girl);
}

结构体嵌入结构体

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

struct st_pet
{
    char name[21];
    char type[21];
};

struct st_girl
{
    char name[21];
    int age;
    double weight;
    char sex;
    bool yz;
    struct st_pet pet;
};

int main()
{
    st_girl girl = {"bigDavid",23,50.5,'X',true,{"as","ass"}};
    cout << girl.name << ' ' << girl.pet.type << ' ' << girl.pet.name << endl;
}

7 结构体中的指针

如果结构体的指针指向的是动态分配的内存地址:

(1)对结构体用sizeof没有意义

(2)对结构体用memset()函数可能会造成内存泄露(申请了堆区指针不释放)

在没有动态分配内存之前,是可以用memset()函数清空结构体

在没动态分配内存之后,逐个清空

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

struct st_t
{
    int a;
    int* p;
};

int main()
{
    st_t stt;
    // memset(&stt, 0, sizeof(st_t));

    stt.a = 3;
    stt.p = new int[100];

    cout << sizeof(stt) << endl; // 16
    cout << stt.a << ' ' << stt.p << endl;
    // 清空成员a
    stt.a = 0;
    // 清空成员p指向的内存中的内容
    memset(stt.p, 0, 100*sizeof(int));
    cout << stt.a << ' ' << stt.p << endl;
    delete[] stt.p; 
}

(3)C++中的string中有一个指向的是动态分配的内存地址指针

cpp 复制代码
struct string
{
	char *ptr;	
};

memset一个带有指针成员类型是未定义的行为,不同编译器处理不一样,行为未定义结果肯定也未定义。string是c++封装的一个带有指针成员的类

string类的构造函数在构造string对象时,str_data指针会指向堆中字符串长度加1大小的内存空间,而使用memset函数对string类型对象清零后str_data变成了0,指向原内存空间在析构函数中不会被释放,导致内存泄漏。

不要轻易0初始化string、vector等STL标准容器及具有动态内存管理的类

cpp 复制代码
#include <iostream>
#include <cstring>
using namespace std;
struct st_girl
{
    string name;
};
int main()
{
    st_girl girl;
    girl.name = "BigDavid";
    cout << girl.name << endl;
    //memset(&girl, 0, sizeof(girl));
    girl.name = "Liu";
    cout << girl.name << endl;
}
相关推荐
南东山人1 小时前
一文说清:C和C++混合编程
c语言·c++
Red Red3 小时前
网安基础知识|IDS入侵检测系统|IPS入侵防御系统|堡垒机|VPN|EDR|CC防御|云安全-VDC/VPC|安全服务
网络·笔记·学习·安全·web安全
Ysjt | 深4 小时前
C++多线程编程入门教程(优质版)
java·开发语言·jvm·c++
ephemerals__4 小时前
【c++丨STL】list模拟实现(附源码)
开发语言·c++·list
Microsoft Word4 小时前
c++基础语法
开发语言·c++·算法
Natural_yz4 小时前
大数据学习17之Spark-Core
大数据·学习·spark
qq_172805595 小时前
RUST学习教程-安装教程
开发语言·学习·rust·安装
一只小小汤圆5 小时前
opencascade源码学习之BRepOffsetAPI包 -BRepOffsetAPI_DraftAngle
c++·学习·opencascade
虾球xz5 小时前
游戏引擎学习第20天
前端·学习·游戏引擎