系统学习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;
}