《斯坦福大学:编程范式》第三节:* 与&的技巧,每种数据结构的内存布局和内存地址
---------------例1---------
double d = 3.1416;
char c = *(char*) &d;
&d 拿到指向d的内存地址,根据内存的起始点不同,值不同。
(char*) 把它当做 (char*) 类型,也就是指向char的指针。
* 解引用:根据指向的地址的起始点,向后 拿 8bits(也就是 char的内存大小,1字节 Byte)
结果是 会取 d的前八位。翻译为 char类型的值。
---------------例2-----------------------------
short s = 45;
double d = *(double*)& s;
跟上文一样,因为double 是 8字节,
所以 从 s第一个字节往后取 8字节。
但s本身是short,只有2字节,
所以,取完s的内存,还会往后取6字节。
如果后面有内存,则会拿到完整的8字节,翻译为double;
如果没有,则会导致程序崩溃。
--------struct-------------------------
struct fraction{
int num;
int denum;
}
占用4+4=8字节。 两个int的内存分配紧密挨着。 num的地址在下面,denum的地址在上面(因为num先存储)
结构体的地址 = 第一个域的地址,也就是 num的地址。
-----------------例子1----------------------------
pi.num =22;
pi.denum=7;
fraction * & (pi.denum) ->num = 12;
cout<<pi.num<<endl;
cout<<pi.denum<<endl;
1. 运算符从右往左,写作 ( fraction *) & (pi.denum) 更清晰。
& ( pi.denum) 拿到指向结构体pi的第二个域denum的开头的指针
( fraction *) 当作 一个struct来解析,所以 内存会继续向上寻找4位来作为一个新的结构体,我们可以命名它叫 pi2
然后,上面的程序实际上是 pi2 ->num = 12;
pi2的num实际上就是 pi的denum,他们的位地址完全一样。
所以此刻,pi.num 值不变=22, pi.denum 从7变为 = 12
--------指针运算---------------------------------数组-------------
int array[10];
array[0] =44;
array[9] = 100;
在内存中,它是从左往右紧密存储:
44,XXXXXXXX,100 (这里的X表示没赋值)
array的地址 = 第一个元素的地址,也就i是array[0] 。 我们直接写 array 后面没有索引,就表示array数组的地址。
array = & array[0];
array + k = & array[k];
这就是指针运算。跟位运算不同。
这里加K,是表示加 K个数组元素类型的元素。
array 是数组名,实际上它是 int* 类型,
所以 array + k 相当于 位运算里,从数组起点 移动 K * typeof(int) 个字节
---------------例子1-----------------------------------
array [10]= 1;
在java中数组有边界检查,但是C/C++中没有。
---------------------------对数组名 解引用---------------------------
因为数组名 实际上是指针,所以可以解引用:
* array == array[0];
* (array+K) == array[k];
------------例子2:------------
array[-4] = 77
对于int数组。 前进和回退 typeof(int) * (index) 这么多个字节
真正的底层运算是
*(array-4) = 77
指针向左移动4个int单位,然后解引用。
所以我们明确一点:
指针不直接操作bit,也不直接操作bytes,而是以申明的类型 为最小单位!
比如 long* 指针,以 一个long 8bytes 为最小移动和指向单位。
-----------------------例子3:----------------------
int arr = [5]
arr[3] = 128;
(short*)arr[6] =2;
cout<<arr[3]<<endl;
结果是 640;
分析:
(short*)arr[6] == ((short*)arr)[6]
(short*) 把 a 洗脑 ,在内存的二进制上,当做short类型来看待,然后 arr[6]赋值为2,arr[6] 此刻实际上是之前的 int*类型的arr[3]的前面两个字节。
因为 arr[3] =128 ,字节为 0000,0000,0100,0000
所以 前面两个字节被解析为short覆盖了,为 0000,0001,0100,0000 = 640
---------总结:-------我们可以强制转换类型来操作原本操作不到的内存--------------------------------
(short* )( (char*)&arr[1] +8) = 100;
额。。各种转换,只要你乐意。
----------------------------------struct 与数组混合-----------------------------------
struct student{
char* name ;
char snid[8];
int numUnits;
}
student peoples [4];
peoples[0].numUnits = 21;
peoples[2].name = strdup("Adam"); //strdup是内存复制的函数,用于动态分配字符串。
在之前我们说过,在内存布局上,我们把stuct里面的数据布局,看做栈一样的布局。 name是第一个,所以在最底层, snid在中间,numUnits在上面。
对于数组和指针的布局,是从左往右。
整个数组,是存在于栈上
不过上文 动态分配出来的 “Adam” 字符串,是存在于堆上 A,d,a,m,\0 占用了五个字节。name 只是指向堆上的它。 name所在位置,并不写入字符串
peoples[3].name = peoples[0].snid +6;
peoples[0].snid 是一个 char* 类型, 所以 +6 是一个指针操作。
所以 peoples[3].name 指向了 peoples[0].snid 后面+6个char的位置
strcpy (peoples[1].suid, "40415xx" ); // strcpy跟strdup 一样拷贝字符串,但不申请内存。 基于bit 挨个拷贝,直到遇见了0. 所以 suid所在的栈上的位,真的写入了字符串。
--------------打印字符串------- 打印char --------打印地址----------------
char* str = "colleen"; //在内存中表示为 colleen\0 \0是字符串的结尾
cout<< *str <<endl; //输出 colleen
cout<< str <<endl; //输出的还是 colleen
cout<< str+1 <<endl; //输出 olleen
cout<< str+6 <<endl; //遇到0 结束, 所有什么都不打印。
在C++中,字符串是以空终止符('\0')结尾的字符数组,通过字符串中第一个字符的指针访问字符串。也就是说,字符串的值是字符串中第一个字符的(常量)地址。 可以看到,我们打印的 按理说是 str,是一个char*,也就是指针,但是我们并没有打印出它的地址。
因为,如果要输出char string类型的指针的地址,需要把它强转为void* , 也就是无类型,告诉编译器,不要按字符串类型去解释后面的二进制了。
即强制char *转换成void *,那么,char型变量和字符串的地址就可以以十六机制的格式输出了,如下所示:
cout<< static_cast<void *>(str ) <<endl;
cout<< (int *)str<<endl; //也可以输出内存地址,如上所说,只有char* 和string 才需要强转为void* 因为 字符串很特殊,相对数值类型,有"\0“结尾。然而内存地址没结尾
对于 指向 int ,foalt ,long等基本数值类型的指针,则不需要强转为void*
---------对于char是同样的道理--
char ch = 'a';
cout<< &ch <<endl; //这里输出不是‘a’ ,而是乱码。
因为对 char类型取地址 然后输出, 并不能拿到地址,char 跟string 和char*不同, 没有"\0"在内存中多占用最后一字节作为结尾。
所以输出乱码。