文章目录
- 第一章:基础语法
- 第二章:数组与指针
- 第三章:指针(Pointer)
- 第四章 函数
- 第五章 变量的作用域/生命周期/修饰符
- 第六章 String
- 第七章 内存管理
- 第八章 结构体(Struct)
- 第九章 共用(Union)于枚举(Enum)
- 第十章 单向链表
- 第11章 文件(File)
- 十二章 位操作(Bit Operation)
- 十三章 预处理
打算从今天开始学习C++,以下是个人的学习笔记。
第一章:基础语法
1.0 发展历史:
总共可以划分为以下阶段:
- 二进制指令;
- 汇编语言;
- 高级语言(C, C++, java…)
C++ 是简单也是最复杂的一门语言,如果不使用C++的一系列特性, 那么它写起来就很简单;但是既然需要C++,那我们不使用他的特性,那我们学习它干嘛呢? 它的一系列高级特性使得它是一门很复杂的语言。之前有听一位前辈说,C# 敲起来很爽,但是后期优化异常繁琐。但是C++ 前期费劲,可是优化起来就轻松很多了。至于对不对,也是我接下来一段时间想要去研究的一点。
1.1 程序,数据类型:
程序就是一个指令的合集。这些都在以前的 C 的笔记中有提到过,电脑中所有的数据都是以二进制数据储存的,这些都不说了。二进制的转化学习什么的也都是之前学习过的,数据类型也是差不多的,参照原来的笔记适当的去复习了一下下。
1.1.1 变量的声明:
语法与 C 一致:
- 声明变量类型 命名
char ch;
short sh;
int in;
char ch1, ch2;
int in1 = 1, in2 = 2;
关于变量命名的规则,所有语言都差不多。遵守规则的同时,还有考虑可读性就行。
1.2 运算符与表达式:
1.2.1 位运算:
位运算符:
操作符 | 功能 | 用法 |
---|---|---|
~ | 按位非 | ~ expr |
<< | 左移 | expr1 << expr2 |
>> | 右移 | expr1 >> expr2 |
& | 按位与 | expr1 & expr2 |
^ | 按位异或 | expr1 ^ expr2 |
&= | 按位与赋值 | expr1 &= expr2 |
^= | 按位异或赋值 | expr1 ^= expr2 |
- 关于 ~ 的运算就是按位全部取反:
~01111111 = 10000000; - 关于 & 的运算就是按位全部取与运算:
0101 & 1101 = 0101; - 关于 ^ 运算就是按位全部取异或运算:
0101 ^ 1101 = 1000; - 关于 | 就是全部取或运算:
0101 | 1101 = 1101;
==============================================================
-
关于 << 运算就是把所有的位向左移动,空余的补0:
00000001 << 1 = 00000010
00000001 << 2 = 00000100
00000001 << 3 = 00001000 -
关于 >> 的运算就是把所有的位向做移动, 空余的补0:
10000000 >> 1 = 01000000
10000000 >> 2 = 00100000
10000000 >> 3 = 00010000
左移一位相当于这个数乘以2的1次方;
左移两位相当于这个数乘以2的2次方;
左移三位相当于这个数乘以2的3次方;
左移四位相当于这个数乘以2的4次方;
。。。
右移则是除以就好;
==============================================================
-关于 &=, ^=, |= 的大致用法与 +=, -=都差不多,整个表达式的值也是符号右边的值:
int a = 0;
int b = 1;
int c = a &= b;
cout << c << endl;
c 的值就是a的值,输出为 0;
1.3 循环的使用
1.3.1 for循环
这个其实大部分的语言都有这么 for 循环的用法,这里还是记录一下。for 循环其本质也是简洁的需要(简洁对于计算机的使用真的太重要了):
int sum = 0;
for (int i = 0; i <= 100; ++i) {
sum += i;
}
cout << " 1 - 100的总和是 " << sum << endl;
这里在括号内声明的 i 是一个作用域只在这个for循环内部的局部变量,只能在 for 循环以内使用。
或者以下声明也可以,唯一的区别就是 j 和 i 的作用域不同,但他俩都是局部变量:
int j = 0;
for ( ; j <= 100; ++j) {
sum02 += j;
}
cout << " 1 - 100的总和是 " << sum02 << endl;
然后是下面这种写法因为可以的:
int k = 0;
for ( ; k <= 100; ) {
sum03 += k;
++k;
}
cout << " 1 - 100的总和是 " << sum03 << endl;
随后试了一下, for循环里是否可以什么都不添加,发现是可以的,只不过是一个死循环,将一直循环下去,如果硬要用这种形式写的话:
int g = 0;
for ( ; ; ) {
if (g > 100) {
break;
}
sum04 += g;
++g;
}
cout << " 4: 1 - 100的总和是 " << sum04 << endl;
条件语句来break出去;
1.3.2 continue的用法:
计算 1 - 100 中,能被5整除的数:
int num01 = 0;
for (int i = 1; i <= 100; ++i) {
if (!(i % 5)) {
cout << i << endl;
++num01;
}
}
cout << "1: 0 - 100 中能被5整除的数有 " << num01 << endl;
或者我们也可以用 continue 来实现:
int num02;
for (int i = 1; i <= 100; ++i) {
if (i % 5) {
continue;
}
cout << i << endl;
++num02;
}
cout << "2: 0 - 100 中能被5整除的数有 " << num02 << endl;
当 i 不能被 5 整除时,通过 continue 跳转到 for 里的 ++i,然后开始新的循环;
1.3.4 作用域:
当我们在声明变量的时候,每个变量都会有一个他自己的生命周期,通常来说,一个变量的生存周期就是在它被声明的时候起,到往后离它最近的一个大括号结束。这就是变量的生命周期。
1.3.5 while 循环
while( 表达式 ){
}
若括号内的表达式值为真,那么就执行大括号内的内容;
int sum = 0, int i =0;
while(i ++ < 10) {
sum += i;
}
当然也可以结合 continue 和 break 做到。
1.3.6 do{ } while();
与 while 类似,不同的是,do{ } while () 始终执行一此,然后再判断是否执行。类似上一个输出1 到 10 的总和,就可以用do while 来写:
int k = 1, sum02 = 0;
do {
sum02 += k;
k++;
} while (k <= 10);
cout << sum02 << endl;
1.3.7 跳转
-
break: 不能单独使用, 经常在switch里跳出;在循环里,经常跟在 if () 跳出循环。一般不会无缘无故的使用,且break只能跳出当前循环及一层循环
-
continue: 使用就像是一个筛子,过滤掉不必要的内容。也是结束当前循环及一层循环。
-
return: 退出当前函数。但是再main函数中比较特殊,是直接结束程序。 同时包含多个return的话,以第一个为准。
-
goto: 属于慎用的一个短跳转,属于很矛盾的存在,有它在基本推翻了所有之前所学的循环,对代码的结构性有极大的破坏性,不过偶尔用用还是很舒服的,比如跳出多重循环,效率特别的高。如果按照平常break层层跳出,代码会非常繁琐,我们需要声明多个flag来判断,不简洁,效率低,但是有了goto可以直接跳出多重循环。还有就是可以用于集中错误处理。
第二章:数组与指针
2.0.1:一维数组定义大小初始化
这是学习的第一种构造类型,这个构造类型是相对于之前的基本类型来说的,构造类型就是由基本类型所构造出来的产物,而不是凭空出现的。
就好比基本类型是一块砖,而构造类型是一面墙。
数组干嘛用呢:
- 当我们需要一个敌人军队,我们可以一个一个声明军队中的敌人:int enemy1; int enemy2; int enemy3… int enemy100; 这时候就可以用数组来搞定:int enemy[100];
及方便于书写,也方便于管理。
- 定义:
需要类型,命名,以及大小:
int arr[10];
相同的数据类型进行构造就组成了数组,不同的数据类型构造就组成了结构体。
这里简单说一下结构体,之后还会细学一次:
结构体的声明于定义:
struct {
int a = 1; char b = a; double c = 3; }testStru;
结构体的访问:
cout << testStru.a << endl;
- 数组的初始化与赋值:
1.不初始化;》》》》》》成员初始值未知
int arr[10];
2.全初始化
int arr[10] = {
1,2,3,4,5,6,7,8,9};
3.部分初始化;》》》》》未初始化的部分,自动清零
int arr[10] = {
1}
4.不指定大小初始化;》》经常出没
int arr[] = {
0,1,2,3,4,5,6};
C++里不可以越界初始化,但是可以越界访问,越界访问的数据的值是不确定的。凡是构造类型,要么在定义时初始化,不可以先定义再以初始化的方式赋值;凡是基本类型,既可以在定义时初始化,也可以先定义,在赋值。:
int arr[10];
arr[10] = {
0,1,2,3,4,5,6,7,8,9,};
这样是错误的,arr[10]arr数组里的第十个成员,赋值运算符两边的类型是不对等的。
2.0.2 一维数组的逻辑与存储
- 一维数组在内存中是一段连续的储存区域。
如何证明一维数组的储存空间在内存中是连续的呢?
int testSaving[10] = { 0,1,2,3,4,5,6 };
for (int i = 0; i < 7; i++) {
cout << i << " 的地址是 " << &testSaving[i] << endl;
}
可以看到,他们储存的地址是连续的,且第一位成员地址是最下面的。
数组的命名不仅仅代表了一个数组构造类型,还要参与元素的访问,此时代表首元素的地址。[ ] 实际上是基址运算符,指偏移了多少个地址。
在 int arr[10] 这个声明中,int就是步长且为4,arr就是起始地址,10就是范围。起始地址,步长,以及范围是数组的三要素。
2.1.0 数组的运用
2.1.1 数组求和与平均值
int practice01[10] = {
0,1,2,3,4,5,6,7,8,9 };
int sum = 0;
for (int i = 0; i < 10; i++) {
sum+=practice01[i];
}
cout <<"这一组数的总和是:"<< sum << endl;
cout << "这一组数的平均值是:" << (float)sum / (sizeof(practice01) / sizeof(int)) << endl;
2.1.2 数组的最值
int practice02[5] = {
};
int lengthOfArr = sizeof(practice02) / sizeof(int);
for (int i = 0; i < lengthOfArr; i++){
cout << "请输入第 " << i+1 << " 个数" << endl;
cin >> practice02[i];
}
int minNum = practice02[0], maxNum = practice02[0];
for (int i = 0; i < 5; i++) {
maxNum = practice02[i] >= maxNum ? practice02[i] : maxNum;
minNum = practice02[i] <= minNum ? practice02[i] : minNum;
}
cout << "这一组数中,最大的数是 " << maxNum << endl << "这一组数中,最小的数是 " << minNum << endl;
2.1.3 排序
- 选择排序:
int selectSort[N] = {
4,2,1,3,5,8,7,9,0,6 };
int lengOfSel = sizeof(selectSort) / sizeof(int);
int temp = 0;
for (int i = 0; i < 9; i++) {
for (int j = i+1; j < 10; j++) {
if (selectSort[i] > selectSort[j]) {
temp = selectSort[j];
selectSort[j] = selectSort[i];
selectSort[i] = temp;
}
}
}
- 选择排序Pro:
比较不换,只记下标,只换最小值的下标;
int newSort[N] = {
3,6,8,1,0,5,3,2,9,33 };
int newTemp = 0;
for (int i = 0; i < 9; i++) {
int idx = i;
for (int j = i+1; j < 10; j++) {
if (newSort[j] < newSort[idx]) {
idx = j;
}
}
if (idx != i) {
cout << newSort[i] << "交换" << newSort[idx] << endl;
newSort[idx] ^= newSort[i];
newSort[i] ^= newSort[idx];
newSort[idx] ^= newSort[i];
}
}
2.1.3 查找
- 线性查找
就是从头找到尾:
int findArry[N] = {
0,1,2,3,4,5,6,7,8,100 };
int findNum = 0;
int newIdx = -1;
cin >> findNum;
for (int i = 0; i < 10; i++) {
if (findArry[i] == findNum) {
newIdx = i;
}
}
if (newIdx == -1) {
cout << "没有找到" << endl;
}
else {
cout << "索引是 " << newIdx << endl;
}
- 折半查找:
要求数组是有序为前提:
int findArry[N] = {
0,1,2,3,4,5,6,7,8,100 };
int right = N - 1, left = 0, mid = -1, newFindNum = 0, ifFind=0;
cin >> newFindNum;
while (right >= left) {
mid = (right + left) / 2;
if (findArry[mid] == newFindNum) {
ifFind = 1;
break;
}
else
{
if (newFindNum < findArry[mid]) {
right = mid - 1;
}
else {
left = mid + 1;
}
}
}
if (ifFind) {
cout << "下标是 " << mid << endl;
}
else {
cout << "没有找到" << endl;
}
2.2.0 二维数组
- 二维数组的本质也是一个数组,只不过相当于每个一维数组中的每个元素又是一个一维数组而已。
- int[4] arry[3] => int arry[3][4],相当于一个三行四列的表格一样。
- 声明/定义:
int arr[3][4];
- 初始化:
- 满初始化:
int arr[3][4] = {
{
1,2,3,4}, {
4,5,6,7}, {
7,8,9,0}};
- 未初始化:
随机值; - 部分初始化:
情况一(行部分初始化):
int arr[3][4] = {
{
1,2}, {
4,5,7}, {
7,8,}};
会在每一个一维数组里缺失的部分填入0;
情况二(整体部分初始化):
int arr[3][4] = {
1, 2 , 4, 5, 7, 7,8};
会依次按照步长来凑齐前面的一维数组;
总结就是,一维数组的数组名是一个一级指针,二维数组的数组名是一个数组指针。
2.2.1 二维数组的数据形态
- 我们主要在二维数组中研究二维平面中的逻辑。
- 主对角线与此对角线的输出:输入一个 4x4 的二维数组,并输出该数组的主对角线和此对角线上的元素:
for (int i = 0; i < 4; i++) {
int temp = i;
while (temp--){
cout << " ";
}
cout << chaseArry[i][i] << endl;
}
for (int i = 0; i < 4; i++) {
int temp2 = 3-i;
while (temp2--) {
cout << " ";
}
cout << chaseArry[i][3-i] << endl;
}
- 逆置一个二维字符数组:将一个 4x4 矩阵进行逆置处理,要求初始化原始矩阵,输出原矩阵和逆置后的矩阵。
char temp3;
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
if (i > j) {
temp3 = charsArry[i][j];
charsArry[i][j] = charsArry[j][i];
charsArry[j][i] = temp3;
}
}
}
- 天生棋局:生成一个 10*10 的棋局,要求,初始化为 0, 随机置入10颗棋子,棋子的位置为1,并打印:
srand((int)time(0));
int newChas[10][10] = {
0 };
int count = 10;
while(count--) {
int ramX = rand() % 10, ramY = rand() % 10;
while (newChas[ramX][ramY]) {
ramX = rand() % 10;
ramY = rand() % 10;
cout << "重复了 " << "X:" << ramX << " Y:" << ramY << endl;
}
newChas[ramX][ramY] = 1;
}
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
cout << newChas[i][j] << " ";
}
putchar(10);
}
或者
srand((int)time(0));
int newChas[10][10] = {
0 };
int count = 0;
while(1) {
int ramX = rand() % 10, ramY = rand() % 10;
if (newChas[ramX][ramY] != 1) {
newChas[ramX][ramY] = 1;
count++;
if (count == 10) {
break;
}
}else{
cout << "重复了 " << "X:" << ramX << " Y:" << ramY << endl;
}
}
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
cout << newChas[i][j] << " ";
}
putchar(10);
}
- 或者可以用continue来实现:
srand((int)time(0));
int newChas[10][10] = {
0 };
int count = 10;
while(count--) {
int ramX = rand() % 10, ramY = rand() % 10;
if (newChas[ramX][ramY] == 1) {
cout << "重复了 " << "X:" << ramX << " Y:" << ramY << endl;
continue;
}
else {
newChas[ramX][ramY] = 1;
}
}
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
cout << newChas[i][j] << " ";
}
putchar(10);
}
- 判断上面生成的棋局是否是好棋:
关键思路就是我们需要扫描我们的棋盘,先一行一行进行扫描,如果有连续5个一横排,就是好棋,如果横排没有那么我们就竖着扫描。
int chaseCount = 0;
int flag = 0;
int flagCol = 0;
for (int i = 0; i < 10; i++) {
chaseCount = 0;
for (int j = 0; j < 10; j++) {
if (chaseBd[i][j] == 1) {
chaseCount++;
if (chaseCount == 3) {
flag = 1;
break;
}
}
else {
chaseCount = 0;
}
}
if (flag == 1) {
break;
}
}
chaseCount = 0;
for (int i = 0; i < 10; i++) {
flagCol = 0;
for (int j = 0; j < 10; j++) {
if (chaseBd[j][i] == 1) {
chaseCount++;
if (chaseCount == 3) {
flagCol = 1;
break;
}
}
else {
chaseCount = 0;
}
}
if (flagCol == 1) {
break;
}
}
if (flag == 1 || flagCol == 1) {
cout << "flag: " << flag << " flagCol: " << flagCol << endl;
cout << "好棋" << endl;
}
-
五子棋的输赢判断:
在五子棋中,除了判定横竖的范围外,我们还需要判定类似于以上斜着的获胜条件。因为并不是所有的斜获胜都能行,所以在10 x 10的棋盘中,有以上的一个范围。 -
有序数组归并
合并两个已经有序的数组A[M], B[N],到零为一个数组C[M+N]中去,使另外一个数组依然有序,其中M和N均是宏常量。
int A[M] = {
1,34,65,76,80 };
int B[N] = {
2,4,6,8 };
int C[M + N];
int i = 0, j = 0, k = 0;
while (i < M &&j < N) {
if (A[i] < B[j]) {
C[k++] = A[i++];
}
else {
C[k++] = B[j++];
}
}
while (i < M) {
C[k++] = A[i++];
}
while (j < N) {
C[k++] = B[j++];
}
for (int i = 0; i < M + N; i++) {
cout << C[i] << " ";
}
2.2.2 数组名的二义性
数组名是数组的唯一标识符。
- 数组名充当一种构造类型
int arr[10];
cout << "sizeof(arr[10]) = " << sizeof(arr) << endl;
cout << "sizeof(int[10]) = " << sizeof(int[10]) << endl;
- 数组名充当访问数据成员的首地址
int arr[10] = {
2};
cout << "arr = " << arr << endl;
]cout << "&arr[0] = " << &arr[0] << endl;
cout << "*(arr+0) = " << *(arr+0) << endl;
第三章:指针(Pointer)
指针本身并不复杂,但是指针的难点在于它将与我们学习过的所有的数据类型都产生关系。所有的数据都被我们储存在内存里。而指针又是直接操作内存的。所以它是天使也是魔鬼,后期所有的bug,崩溃等问题基本都与指针有关。
3.1 认识内存
3.1.1 线性内存
之前学习的那些一维数组,二维数组都仅仅是逻辑上的体现,最终数据都是要保存到内存当中的,而内存又是线性的,内存的线性是物理基础。
之前我们看到看到的二维数组都是一种行列的表格形式,但是那仅仅是我们在逻辑上的一种表达方式:
但是内存一种线性的存在,所以他实际在内存中的储存形式是这样的:
我们知道一维数组的逻辑和储存都是一致的,均是线性的,而二维数组的逻辑是二维的,但是其储存是线性的。储存的线性原因就是内存的物理特质所决定的。
int arr[3][4] = {
1,2,3,4 };
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
cout << &arr[i][j] << endl;
}
putchar(10);
}
有此可见数组在内存中是一段连续的储存空间。
3.1.2 变量的地址与大小
如上图所示,一个格子代表一个内存。在一段内存中,int类型包含了4个地址,double包含了8个地址等,那么我们在取地址的时候,到底取的是哪一个地址呢?我们拿的都是低位字节的那一个地址,也就是每个类型中最下面那一个地址。
32位机的前提下,每个内存的大小都是4个字节,64位则是8个字节。
char a = 1;
short b = 2;
int c = 10;
double d = 123.45;
printf("&a = %p\n", &a);
cout << "&b = " << &b << endl;
cout << "&c = " << &c << endl;
cout << "&d = " << &d << endl;
3.2 指针常量
其实我们在上一节中对一个变量取地址取出的地址就是一个指针了,且是一个常量指针。那么既然取出的地址就是一个指针,但是我们取出的地址往往只是一个单纯的地址而已,真正的常量指针,还需要加上指针类型。所以指针的本质是一个有类型的地址,然而类型决定了从这个地址开始的寻址能力。
3.3 指针变量
但凡与*扯上关系的,都是和指针扯上关系了的,并且一个指针类型的大小都是4个字节,因为只是储存的一个地址。
char a; short b; int c; float d; double e;
cout << sizeof(char *) << endl;
cout << sizeof(short *) << endl;
cout << sizeof(float *) << endl;
cout << sizeof(int *) << endl;
cout << sizeof(double *) << endl;
- 声明一个指针:
type * pointerName
*表明了本变量指针变量,大小,类型决定了该指针变量中的地址的寻址能力。
声明一个指针常量,必须要保存两样东西,一个地址数据,一个类型。
- 寻址能力的研究:
int data = 0x123456;
int *pdata = &data;
cout << hex;
cout << *pdata << endl;
cout << hex;
cout << *(int*)pdata << endl;
printf("%x\n", *(char*)pdata);
cout << hex;
cout << *(short*)pdata << endl;
** - 指针的本质 就是有类型的地址。**
** - 类型又代表着寻址能力。**
** - 所以当我们比较两个指针时,不仅要比较其所指地址,还要比较其类型。**
3.3.1 指向/被指向/更改指向
我们通常进行口述表达时说,谁指向了谁,就是一种描述指针的指向关系。指向谁,就代表保存了谁的地址。
- 指向谁就是保存了谁的地址
int a = 43;
int* pt_a = &a
cout << "&a = " << &a << endl;
cout << "pt_a =" << pt_a << endl;
- 更改指向
指向是可以更改的,就好比一个巫毒娃娃,我今天可以用它来操控小明,明天可以用它来操控小刚;
int a = 43;
int* pt_a = &a;
int b = 305419896;
int *pt_b = &b;
cout << "&a = " << &a << endl;
cout << "pt_a = " << pt_a << endl;
pt_a = &b;
cout << "pt_a = " << pt_a << endl;
cout << "&b = " << &b << endl;
3.3.2 NULL
8.3.2.1 野指针(Invalid Pointer)
也就是无效指针,我们习惯性称它为野指针。因为是一个非常危险的东西,就像野熊,野狼一样的存在。常见情形有两种,一是未初始化的指针, 二是指向已经被释放的空间。
- 关于未初始化的指针为什么危险呢
int* pt_a;
*pt_a = 100;
像以上的例子,我们声明了指针哟吼并没有去初始化它,导致后面去使用它时操纵了一段未知空间的值。一般来说,去读一个野指针问题还不大,但是去写一段野指针往往会被系统拦截或者引发程序崩溃,但是!!!!你若是对一个野指针写入成功了,这造成的后果是无法估量的。
所以我们需要养成一个习惯,就是没声明一个指针,哪怕我们不会马上适用它,我们也要把它声明成一个NULL指针。
3.3.2.2 NULL指针
int* pa = NULL;// NULL (void*) 0;
实际就是约定内存中专门为未初始化的指针的这么一个标记位。官方说法是NULL指针既不能读,也不能写。
3.4 指针运算
指针能参与运算的并不多,但是非常的特别。
3.4.1 赋值运算
不兼容类型的赋值会发生类型丢失,为了避免隐式转换带来可能出现的错误,最好用强制转换显示的区别。
3.4.2 算术运算
指针的算术运算,不是简单的数值运算,而是一种数值加类型运算。将指针加上或者减去某个整数值(以n*sizeof(T)为单位进行操作的)。
前面提到说,指针算术运算是一种数值加类型的一种运算,什么意思呢:
int * i = (int *)0x0001;
short * s = (short *)0x0001;
double * d = (double *)0x0001;
cout << "i = " << i << " i+1 = " << i + 1 << endl;
cout << "s = " << s << " s+1 = " << s + 1 << endl;
cout << "d = " << d << " d+1 = " << d + 1 << endl;
我们能得到结果:
可以总结出,指针算术加减的是步长,也就是指针类型的大小。指针实际就是 类型(步长)+地址(物理数据);
- 具体理解可以通过下面这个例子来理解:
int arr[10];
int * pHead = &arr[0]; int * pTail = &arr[9];
int address = (int)&arr[9] - (int)&arr[0];
cout << address << endl;
cout << pTail - pHead << endl;
我们得到的输出如下:
int 类型的指针每加一是加4个数值。实际就可以看成指针的是在储存单元里按照类型大小来进行运动的。
值得一提的是,只有当指针指向一连串连续的储存单元时,指针的移动才有意义。
3.4.3 关系运算
注意,于C不同,C++不同类型的指针是不能进行比较的
指针的关系运算有什么用呢,有了上面的学习吗,现在可以重新来做回文判断了:
判断一char类型数组是否是一个回文数组:
char name[5] = {
'M', 'A', 'D', 'A', 'M' };
char * ptrCharL = &name[0];
char * ptrCharR = &name[4];
int ifTrue = 1;
while (ptrCharR < ptrCharL) {
if (*ptrCharR == *ptrCharL) {
ptrCharL++;
ptrCharR--;
}
else {
ifTrue = 0;
break;
}
}
if (ifTrue) {
cout << "是的" << endl;
}
else {
cout << "不是" << endl;
}
3.5 数组访问
3.5.1 偏移法,本质法
数组名其实就是一个指针,这个例子最能体现数组名就是指针这一本质。
int arr[10] = {
1,2,3,4,5,6,7,8,9,10 };
for (int i = 0; i < 10; i++) {
cout << *(arr + i) << endl;
}
3.5.2 下标法
最不能体现数组名本质的方法,但是很直观。
int arr2[10] = {
11,22,33,44,55,66,77,88,99,1010 };
for (int i = 0; i < 10; i++) {
cout <<
3.5.3 一维数组名可以赋给一级指针
先做两个铺垫:
数组名是一个常量指针
能用数组名解决的问题,都能用指针来解决,能用指针解决的问题,一定能用数组名解决,数组名解决不了的事情,指针也可以解决。
数组名就如同孙悟空的金箍棒,也就是定海神针,它是一个常量指针,你试图更改它,那这片海就出大事了。
cout << "arr = " << arr << endl;
cout << "arr+1 = " << arr + 1 << endl;
cout << "&arr[0] = " << &arr[0] << endl;
cout << "&arr[0]+1 = " << &arr[0] + 1 << endl;
由此可见,之前提到过的,数组名就是一个指针,而指针又是一个数据的地址。数组名所储存的就是arr[0]的地址。
int * pa = arr;
cout << pa << endl;
这里有的人可能就就会认为,**二维数组名可以赋值给一个二级指针,这种说法是错误的!**二维数组名就是一个单纯的数组名,二维数组是一个指向指针的指针,两个东西不是一个类型!
3.6 二维数组与指针
有这么一个数组:
int a[3][4] ={
11,12,13,14,
21,22,23,24,
31,32,33,34
};
从 a 到 a[0] 再到 a[0][0] 到底经历了什么呢
第四章 函数
我们为什么使用函数呢,总结起来就是以下几点好处:
- 可以提高程序开发效率。
- 提高了代码的重用性。
- 使程序变得更简短而清晰。
- 有利于维护。
4.1.1 rand()
rand()函数能生成一个随机数,但是这个随机数是一个伪随机数,你会发现每一次运行以下代码:
int randNum = rand();
cout << randNum << endl;
输出结果都是41。
所以为了达到真正的随机数效果,我们通常都会一起使用 srand(unsinged int seed) 函数一起使用。它的作用是初始化随机数生成器。参数seed就是一个给随机数生成器的整形数种子。每一次生成随机数都是在这个种子的基础上套用算法叠加出来的随机数,所以在计算机里压根不存在真正的随机数,这也就是彩票不使用计算机开奖的原因了。那么我们要怎么办才能尽力拿到一个近似随机数呢?
在计算机世界中,也有类似于纪念日的这么一个日期,也就是世界上第一台操作系统诞生的日期,1979-1-1 零点。我们添加 ctime 库,然后在srand中添加(time(0)):
srand((int)time(0));
这个time(0)就是从计算机元年到现在的秒数,并且是一直在变化的。但是如果你运行的足够快,两次运行结果是不变的。
在这里可以尝试写一下往一个数组里添加不同的随机数了。
我写的过程简单,就是用重复的话计数退位,然后两个break搞定。如下:
int randArr[10];
int count = 0;
while (1) {
int randNum = rand() % 10;
randArr[count++] = randNum;
for (int i = 0; i < count - 1; i++) {
if (randArr[i] == randArr[count-1]) {
count--;
break;
}
}
if (count == 10) {
break;
}
}
那如果我们要求对[100,200]取随机数呢,我们只需要对随机数做处理就行了,随机0-100的随机数在加100就行了。或者500 - 900 之间的随机数,也可以:
int arr[400];
int count = 0;
while (1) {
int randNum = rand() % 401 + 500;
arr[count++] = randNum;
for (int i = 0; i < count - 1; i++) {
if (arr[i] == arr[count - 1]) {
count--;
break;
}
}
if (count == 400) {
break;
}
}
注意这里是双边包含的中括号 [ ],所以取0-400的数应该是对401取模。
4.1.2 常用函数库
4.2 自定义函数
我们在写自定义函数的时候,推荐先将函数调用写出来,这样做的原因是在一开始就把所有的调用方法确定下来了,类似于函数名啊,参数啊之类的。比如我们要写一个求两数中最大数的函数:
4.2.1 定义与声明
int main() {
int a = 10,b = 23;
int iMax = FindMax(a, b);
cout << "最大值 = " << iMax << endl;
system("pause");
return 0;
}
这样定义就出来了,然后再去写函数:
int FindMax(int a, int b) {
return a > b ? a : b;
}
int main() {
int a = 10,b = 23;
int iMax = FindMax(a, b);
cout << "最大值 = " << iMax << endl;
system("pause");
return 0;
}
在这里我们就可以区分定义和声明的区别了,之前我没有区分过声明和定义的具体区别,在这里就可以具体说一下区别了。