文章目录
C++学习的第二章,这里对之前学过的内容进一步的提升
第一章 数据类型
1.1 内存
内存是线性的,最小储存单位为比特 (Bit) ,是按字节(Byte)为单位进行编址的,以32位机为例,编制形式如下:
1.2 补码
1.2.1 运算规则
补码的运算规则是互逆的,补码其实就是二进制的一种编码方式,一种编码规则,凡是编码规则的存在,就必定带来一定的取值范围,取值范围的存在就会影响后面的运算,也影响了数据类型的范围。
补码的一大特点就是,能使加减乘除运算一并使用加法表示。
1.3 数据类型
1.3.1 数据类型
1.3.2 数据类型是对内存的格式化
数据类型是对内存的格式化,这一句话很重要。数据类型提供了,申请内存单元的大小和访问规则。数据类型是架设在线性内存 上的一种逻辑关系。什么是格式化呢,就是先给它一定的格式,再去储存访问它。
1.4 类型转化
1.4.1 类型转化的原理
1.4.1.1 小数据赋给大变量
不会造成数据的丢失,系统为了保证数据的完整性,还提供了符号扩充行为。这方面之前举过一个很不恰当的例子:小运载卡车将货物放到大运载卡车上,示意图如下:
1.4.1.2 大数据赋给小变量
会发生 Truncate(截断行为),有可能会造成数据丢失。
int main(int argc, char * argv[])
{
int a = 0x5334fc;
short b = a;
cout << a << endl;
cout << 0x34fc << endl;
cout << b << endl;
system("pause");
return 0;
}
大数据赋给小变量时,相当于先将一个short类型大小的内存从最低位开始套在大数据上,然后截断。
1.4.2 隐式转化
1.4.2.1.整型提升
在 32 位机中,所有位为低于 32 的整型数据,在运算过程中先要转化为 32 位的 整型数据,然后才参与运算。
1.4.2.2 混合提升
引用一段来自KR的话:
- First, if either operand is long double, the other is converted to long double.
- Otherwise, if either operand is double, the other is converted to double.
- Otherwise, if either operand is float, the other is converted to float.
- Otherwise, the integral promotions are performed on both operands;
- Otherwise, if either operand is unsigned int, the other is converted to unsigned int.
- Otherwise, both operands have type int
int main(int argc, char * argv[])
{
unsigned int a = 1;
int b = -100;
printf("a + b = %u\n", a + b);
printf("a + b = %d\n", a + b);
system("pause");
return 0;
}
下面一道关于混合提升的练习题:
int main(int argc, char * argv[])
{
char a[1000];
int i;
for (int i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
cout << strlen(a) << endl;
system("pause");
return 0;
}
以上代码的输出结果是多少?
之前有说过,二进制也是使用的模的系统,就像时钟一样,从1点一直走,走一圈就到了2点。
1111 1111开始走,走到下一个1111 1111,刚好就是一圈。所以会输出255.
第二章 进程空间(Process Space)
2.1 进程空间
- command-line arguments and environment variables :放置了命令行参数与环境变量(环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数)。
- stack:Auto类型的变量都在这里
- heap:也就堆,存放的是成员变量,方法之外,类之内,随着对象而产生,随对象销毁而销毁。
- uninitialized data(bss):未初始化数据段
- initialized data:已初始化数据段
- text:是存放了程序代码的数据:在代码段中,也有可能包含一些只读的常数变量,列如字符串常量等
2.2 进程/程序
2.2.1 程序
源文件经编译器,编译后的可执行性文件。程序是一个静态的概念,程序中包含 2 个区域,分别是text / initial data。
2.2.2 进程
进程,被操作系统加载至运行结束的过程。进程是一个动态的概念。进程包含 5 个区域,分别是:text / initial data / uninitial data / heap / stack
2.2.3 进程到程序
2.3 数据在进程空间的存储
2.3.1 示意图
2.3.2 数据在进程空间
2.4 压栈与出栈
要理解压栈与出栈,借助递归可以更好地理解,下面代码使用低轨倒叙打印了一个数组:
void ReverseArr(int* parr, int i, int len)
{
if (i != len - 1)
{
ReverseArr(parr, i + 1, len);
}
cout << parr[i] << endl;
}
int main()
{
int arr[10] = {
1,2,3,4,5,6,7,8,9,10 };
ReverseArr(arr, 0, 10);
system("pause");
return 0;
}
但是,同样的代码,换一下顺序就可以正序打印:
void ReverseArr(int* parr, int i, int len)
{
cout << parr[i] << endl;
if (i != len - 1)
{
ReverseArr(parr, i + 1, len);
}
}
int main()
{
int arr[10] = {
1,2,3,4,5,6,7,8,9,10 };
ReverseArr(arr, 0, 10);
system("pause");
return 0;
}
第三章 数组
3.1 一维数组
3.1.1 本质
数组是用于存储相同数据类型数据,且在内存空间连续的一种数据结构类型,他也是构造类型之一,数组的三要素有:
- 范围
- 步长
- 起始地址
3.1.2 访问
数组名是数组的唯一标识符,数组的每一个元素都是没有名字的。
3.1.3 做参数传递
做参数传递,旨在传递三要素。
void PrintArr(int* arr, int len)
{
for (int i = 0; i < len; i++)
{
cout << *(arr + i) << endl;
}
}
int main(int argc, char* argv[])
{
int arr[4] = {
11,22,33,44 };
PrintArr(arr, 4);
system("pause");
return 0;
}
arr 即代表了起始地址,又代表了步长,len则代表了范围。
3.2 二维数组
3.2.1 本质
二维数组的本质是一维数组,只不过,一维数组的成员又是一个一维数组而己。
三要素:
type name[M][N] = type[N] name[M]
3.2.2 初始化
行可以省略,列不可以初始化
3.2.3 访问
数组名依然是数组的 唯一 标识符。
int arr[3][4] = {
0,0,0,0,
1,1,1,1,
2,2,2,2 };
// TODO
之前我们一直说 * 是取内容, & 是取地址。其实,这种理解并不全面。随后会进行这方面的补充.
3.2.4 线性存储
二维数组在逻辑上是二维的,但是在存储上却是一维的。正是这个特点,也可以用一维数组的方式去访问二维数组的,如下:
//二维逻辑
int main()
{
int arr[3][4] = {
0,00,0,0,
1,1,1,1,
2,2,2,2 };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
cout << arr[i][j];
}
putchar(10);
}
system("pause");
return 0;
}
我们使用一维逻辑去实现:
int main()
{
int arr[3][4] = {
0,00,0,0,
1,1,1,1,
2,2,2,2 };
int* p = (int*)arr;
for (int i = 0; i < 12; i++)
{
cout << p[i] << endl;
}
system("pause");
return 0;
}
以上两种都是一样的。
3.3 数组指针
3.3.1 引入
一次可以移动一个字节指针,称为 char * 指针,一次可以移动两个字节的指针,称为 short * 指针。
一次可以移动一格数组大小的指针,是什么类型的指针,又该如何称呼它呢?比如二维数组名:arr[3][4]; arr+1 一次加1的大小,就是 Int[4] 类型的大小, 即一个数组的大小。
3.3.2 定义
int [N] *pName; = > int( *pName)[N];
3.3.3 typedef
int main(int argc, char* argv[])
{
typedef int(*AP)[4];
AP ap = NULL;
system("pause");
return 0;
}
3.3.4 做参数传递
二维数组本质就是数组指针,所以跟其对应的形参也应该是数组指针类型的变量:
void PrintTwoDie(int(*p)[4], int len)
{
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
cout << p[i][j];
}
putchar(10);
}
}
int main()
{
int arr[3][4] = {
0,00,0,0,
1,1,1,1,
2,2,2,2 };
PrintTwoDie(arr, 3);
system("pause");
return 0;
}
3.3.5 一维空间的二维访问
由于内存是线性的,所以我们可以将其在逻辑上使用二维逻辑。
// 一维空间的二维访问
int main()
{
int arr[12] = {
1,1,1,1,2,2,2,2,3,3,3,3 };
int(*p)[4] = (int(*)[4])arr;
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
cout << p[i][j] << endl;
}
}
system("pause");
return 0;
}
改成2行6列:
int main()
{
int arr[12] = {
1,1,1,1,2,2,2,2,3,3,3,3 };
int(*p)[2] = (int(*)[2])arr;
for (int i = 0; i < sizeof(arr)/sizeof(int[2]); i++)
{
for (int j = 0; j < 2; j++)
{
cout << p[i][j] << " ";
}
cout << endl;
}
system("pause");
return 0;
}
第四章 指针
4.1 内存编址与变量地址
看一下下面的代码:
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
int data = 0x12345678;
int *ptr_data = &data;
printf("%p \n", &data);
printf("%p \n", ptr_data);
printf("%p \n", *ptr_data);
system("pause");
return 0;
}
输出如下:
所以可以总结出,指针的本质就是有类型的地址。
那么上面的 int * ptr_add ,一句废话都没有,总共包括了以下两个方面:
- 声明指针,指定大小( * ptr_add );
- 类型决定寻址能力( int );
还有一点需要注意的是,声明那里的 * ,与 下面输出打印的 * 是不一样的。上面是用来声明指针类型, 而下面则是用来解引用的。
4.2 二级指针
二级指针,是一种指向指针的指针。我们可以通过它实现间接访问数据,和改变一级指针的指向问题。
什么是指向呢,之前也探讨过,指向实际就是保存了它的地址。
4.2.1 定义与初始化
char* *pt = &ch; * 号的位置应该严格按照这种方式去理解。pt 声明了这是一个一级指针,而char 则反映了它的类型。
4.2.2 对数据空间的间接访问
int main(int argc, char* argv[])
{
char ch = 'a', ch2 = 'n';
char* pc = &ch;
char* *pt = &pc;
printf("*pt = %c\n", **pt);
system("pause");
return 0;
}
4.2.3 改变一级指针的指向问题
int main(int argc, char* argv[])
{
char ch = 'a', ch2 = 'n';
char* pc = &ch;
char* *pt = &pc;
*pt = &ch2;
printf("*pt = %c\n", **pt);
system("pause");
return 0;
}
通过二级指针,改变一级指针的指向问题,也相当于在探讨,通过二级指针,对一级指针进行初始化的问题。
4.3.3 初始化一级指针
4.3.3.1 返回错误信息的正确方式
enum {
Success, NameErro, SexErro, StrErro, ScoreErro
};
typedef struct _Stu{
char* name;
char* sex;
char* strNum;
float* score;
}STU;
int InitStu(STU** pp)
{
*pp = (STU*)malloc(sizeof(STU));
(*pp)->name = (char*)malloc(40);
if ((*pp)->name == NULL)
{
return NameErro;
}
(*pp)->sex = (char*)malloc(1);
if ((*pp)->sex == NULL)
{
return SexErro;
}
(*pp)->strNum = (char*)malloc(40);
if ((*pp)->strNum == NULL)
{
return StrErro;
}
(*pp)->score = (float*)malloc(4);
if ((*pp)->score == NULL)
{
return ScoreErro;
}
return Success;
}
int main(int argc, char* argv[])
{
STU* p = NULL;
int ret = InitStu(&p);
if (ret == Success)
{
p->name = (char*)"Damon";
p->sex = (char*)"M";
p->strNum = (char*)"146483983";
*(p->score) = 100.0f;
} else
{
return -1;
}
cout << "Success\n" << endl;
cout << p->name << endl;
cout << p->sex << endl;
cout << p->strNum << endl;
cout << *(p->score) << endl;
system("pause");
return 0;
}
4.3.4 二级指针的步长
所有类型的二级指针,由于均指向一级指针类型,一级指针类型大小是4,所以二级指针的步长也是4,这个信息很重要。
int main(int argc, char* argv[])
{
char* p = 0;
char** pp = &p;
printf("p = %p p+1 = %p\n", p, p + 1);
printf("pp = %p pp+1 = %p\n", pp, pp + 1);
system("pause");
return 0;
}
输出如下:
4.4 指针数组(字符指针数组)
4.4.1 定义
指针数组的本质就是数组,数组中的每一个i成员都是一个指针。形式如下:
语法解析: pArray先与“[]”结合,构成一个数组的定义,char*修饰的是数组的内容,即数组 的每个元素。
4.4.2 二级指针访问指针数组
4.4.2.1 指针数组名赋给二级指针的合理性
二级指针与指针数组名等价的原因:
char * * p 是二级指针;
char* array[N]; array=&array[0]; array[0] 本身是char*型;
char**p=array;
int main(int argc, char* argv[])
{
const char* pa[] = {
"Tencet", "Wangyi","Ali","ByteDance" };
const char** pp = pa;
for (int i = 0; i < sizeof(pa) / sizeof(*pa); i++)
{
cout << *(pp++) << endl;
}
system("pause");
return 0;
}
但是我们还可以进行更进一步的优化。我们可以不依赖数组的长度来遍历了,我们可以判断,当指针指向NULL,遍历就结束了:
int main(int argc, char* argv[])
{
const char* pa[] = {
"Tencet", "Wangyi","Ali","ByteDance" ,NULL};
const char** pp = pa;
while ((*pp))
{
cout << *pp++ << endl;
}
system("pause");
return 0;
}
也可以写成下面这样:
int main(int argc, char* argv[])
{
const char* pa[]