本文声明
本文参考如下系列文章,仅供自学及网友参考
C语言基础
数据类型基本概念:
类型是对数据的抽象; 类型相同的数据具有相同的表示形式、存储格式以及相关操作; 程序中所有的数据都必定属于某种数据类型; 数据类型可以理解为创建变量的模具: 固定大小内存的别名;
数据类型 -- 结构体类型
struct Person1 {
int a;
int b;
};
Person1 *person1 = malloc(sizeof(person1)); //报错
struct Person1 *person1 = malloc(sizeof(person1)); //正确
typedef struct {
int a;
int b;
} Person2;
Person2 *person2 = malloc(sizeof(person2)); //正确
#include <stdio.h>
#include <malloc.h>
struct Person1 {
int a;
int b;
};
int main() {
printf("--------------begin--------------\n");
struct Person1 *person1 = malloc(sizeof(person1));
person1->a = 44;
person1->b = 22;
printf("a= %d, b= %d\n", person1->a, person1->b);
printf("-------------- end --------------\n");
return 0;
}
断言:
--------------begin--------------
a= 44, b= 22
-------------- end --------------
#include <stdio.h>
#include "malloc.h"
typedef struct {
int a;
int b;
unsigned int c;//无符号类型 0~65535
unsigned char d;//无符号类型 0~255
} Person1;
int main() {
Person1 *person1 = malloc(sizeof(Person1));
person1->a = 10;
person1->b = 20;
printf("a = %d\n", person1->a);
printf("b = %d\n", person1->b);
return 0;
}
断言:
a = 10
b = 20
#include <stdio.h>
void test01() {
int p1;
int *p = 99;//定义一个指针,*在左边表示声明一个地址变量,只能存地址(定义了一个地址是99的指针变量),地址里面没有值
printf("p1: %d\n", p1);
printf("p: %d\n", p);//地址是99
printf("*p: %d\n", *p);//指针变量*P,地址的值(没有值叫野指针)
}
int main() {
printf("--------------begin--------------\n");
test01();
printf("-------------- end --------------\n");
return 0;
}
断言:
p1=0
p= 99 地址是99,地址里的值为空
Process finished with exit code -1073741819 (0xC0000005) 野指针报错
#include <stdio.h>
void test01() {
int *p;//定义地址变量,变量是空的
int a = 10;//定义实体变量,变量值为10 (a是有地址的如:0x00ddff)
printf("a的地址: %p\n", &a);
p = &a;//把实体变量a的地址取出来,放到地址变量p中 (p的地址为0x00ddff,与a是相同的)
printf("p的地址: %p\n", p);
printf("p指针的大小: %d\n", sizeof(p));
printf("p的值: %d\n", *p);//取出指针变量的地址,所指向的值
printf("p的地址: %p\n", &p);
}
断言:
a的地址: 000000000022FE14
p的地址: 000000000022FE14
p指针的大小: 8(无论什么类型的指针都是8个字节)
p的值 10
p的地址:000000000022FE18
指针是有类型的
#include <stdio.h>
#include <cdoex.h>
#include <malloc.h>
void test01() {
//指针是有类型的
char *p = NULL;
int *q;
*q = 100l;(错误)//类型转换错误
char *p = NULL;
strcpy(p, "hello");(错误)
}
void test01() {
int a = 0xaabbccdd;
printf("&a= %d\n", &a);//a的地址
int *p1=&a;//定义p1的地址为0xaabbccdd,取地址a里的值,取地址中的四个字节
char *p2 = &a;//取地址中的一个字节
printf("*p1= %x\n", *p1);//p1地址对应的值为a的值
printf("*p2= %x\n", *p2);//地址中存的值的类型和赋值的类型有关
printf("p1= %d\n", p1);//p1变量装的a的地址
printf("p2= %d\n", p2);//p2变量装的a的地址
}
断言:
&a= 6421980
*p1= aabbccdd
*p2= ffffffdd
p1= 6421980
p2= 6421980
指针的步长(指针类型决定步长)
/*指针的步长*/
void test01() {
int a = 0xaabbccdd;
int *p1 = &a;
printf("%x\n", *p1);
char *p2 = &a;//一个字节
printf("%x\n", *p2);
printf("p1的地址: %p\n", p1);//表示同一个地址,地址的最小单位是字节
printf("p2的地址: %p\n", p2);
printf("p1+1的地址: %p\n", p1+1);
printf("p2+1的地址: %p\n", p2+1);
}
断言:
aabbccdd
ffffffdd
p1 的地址:000000000022FE0C
p2 的地址:000000000022FE0C
p1+1的地址:000000000022FE10
p2+1的地址:000000000022FE0D
数组
void test01() {
int c[9] = {1,2,3,4,5,6,7,8,9};
printf("c地址:%p\n", c);
printf("c的第1个元素:%d\n", *c);
printf("c的第2个元素:%d\n",*(c+1));
printf("c地址:%p\n", c);
printf("c的第1个元素的地址:%p\n", &c[0]);
int *p = c;
printf("p中存的c的地址:%p\n", p);
printf("p指针这个指针变量的地址:%p\n", &p);
//数组等于指针,指针等于数组
//数组的遍历
for (int i = 0; i < 9; i++) {
printf("%d\n", p[i]);
printf("%d\n", *(p + i));
printf("%p\n", (p + i));
}
//c 是个常量,只读常量,不能重新赋值
}
断言:
数组指针
/*数组指针*/
void test01() {
int a = 10;
int b = 20;
int c = 30;
int *p1 = &a;
int *p2 = &b;
int *p3 = &c;
int * arr[3] = {p1, p2, p3};//整型数组指针
printf("数组第1个元素对应的值:%d\n", *arr[0]);
printf("数组第1个元素:%p\n", arr[0]);
printf("数组第1个元素:%p\n", *arr);//类似取数组中第一个元素
printf("数组第2个元素:%p\n", *(arr+1));
printf("数组第3个元素:%p\n", *(arr+2));
printf("数组第1个元素对应的值:%d\n", **arr);
char *d = arr;
printf("d的地址:%p\n", *(int *) d);
}
断言:
数组第1个元素对应的值:10
数组第1个元素:000000000022FDFC
数组第1个元素:000000000022FDFC
数组第2个元素:000000000022FDF8
数组第3个元素:000000000022FDF4
数组第1个元素对应的值:10
d的地址:000000000022FDFC
二维数组
void test01() {
//类似java中的二维数组
int a[] = {10,15};
int b[] = {20, 25};
int c[] = {30};
int * arr[3] = {a, b, c};//整型指针
printf("第1个元素对应的值: %d\n", **arr);
printf("对应的值: %d\n", *(*(arr+1)));
printf("%p\n", arr);
printf("%p\n", arr+1);
printf("%d\n", **arr);
printf("%d\n", *(*(arr+1)+1));
printf("%d\n", arr[1][1]);
}
断言:
第1个元素对应的值: 10
对应的值: 20
000000000022FDF0
000000000022FDF8
10
25
25
位运算 可以使用C对变量中的个别位进行操作。您可能对人们想这样做的原因感到奇怪。这种能力有时确实是必须的,或者至少是有用的。C提供位的逻辑运算符和移位运算符。在以下例子中,我们将使用二进制计数法写出值,以便您可以了解对位发生的操作。在一个实际程序中,您可以使用一般的形式的整数变量或常量。例如不适用00011001的形式,而写为25或者031或者0x19.在我们的例子中,我们将使用8位数字,从左到右,每位的编号是7到0。
位逻辑运算符 4个位运算符用于整型数据,包括char.将这些位运算符成为位运算的原因是它们对每位进行操作,而不影响左右两侧的位。请不要将这些运算符与常规的逻辑运算符(&& 、||和!)相混淆,常规的位的逻辑运算符对整个值进行操作。
按位取反~
一元运算符~将每个1变为0,将每个0变为1,如下面的例子:
~(10011010)
01100101
假设a是一个unsigned char,已赋值为2.在二进制中,2是00000010.于是-a的值为11111101或者253。请注意该运算符不会改变a的值,a仍为2。
unsigned char a = 2; //00000010
unsigned char b = ~a; //11111101
printf("ret = %d\n", a); //ret = 2
printf("ret = %d\n", b); //ret = 253
位与(AND): &
二进制运算符&通过对两个操作数逐位进行比较产生一个新值。对于每个位,只有两个操作数的对应位都是1时结果才为1。
(10010011)
& (00111101)
= (00010001)
C也有一个组合的位与-赋值运算符:&=。下面两个将产生相同的结果:
val &= 0377
val = val & 0377
例如:一个数 &1的结果就是取二进制的最末位。这可以用来判断一个整数的奇偶,二进制的最末位为0表示该数为偶数,最末位为1表示该数为奇数。
位或(OR): |
二进制运算符|通过对两个操作数逐位进行比较产生一个新值。对于每个位,如果其中任意操作数中对应的位为1,那么结果位就为1.
(10010011)
| (00111101)
= (10111111)
C也有组合位或-赋值运算符: |=
val |= 0377
val = val | 0377
or运算通常用于二进制特定位上的无条件赋值,例如一个数or 1的结果就是把二进制最末位强行变成1。如果需要把二进制最末位变成0,对这个数or 1之后再减一就可以了。
位异或:^=
二进制运算符^对两个操作数逐位进行比较。对于每个位,如果操作数中的对应位有一个是1(但不是都是1),那么结果是1.如果都是0或者都是1,则结果位0.
(10010011)
^ (00111101)
= (10101110)
C也有一个组合的位异或-赋值运算符: ^=
val ^= 0377
val = val ^ 0377
打开位
已知:10011010:
1.将位2打开
flag | 10011010
(10011010)
|(00000100)
=(10011110)
2.将所有位打开。
flag | ~flag
(10011010)
|(01100101)
=(11111111)
关闭位
flag & ~flag
(10011010)
&(01100101)
=(00000000)
转置位
转置(toggling)一个位表示如果该位打开,则关闭该位;如果该位关闭,则打开。您可以使用位异或运算符来转置。其思想是如果b是一个位(1或0),那么如果b为1则b^1为0,如果b为0,则1^b为1。无论b的值是0还是1,0^b为b.
flag ^ 0xff
(10010011)
^(11111111)
=(01101100)
交换两个数不需要临时变量
//a ^ b = temp;
//a ^ temp = b;
//b ^ temp = a
(10010011)
^(00100110)
=(10110101)
(10110101)
^(00100110)
10010011
int a = 10;
int b = 30;
移位运算符 现在让我们了解一下C的移位运算符。移位运算符将位向左或向右移动。同样,我们仍将明确地使用二进制形式来说明该机制的工作原理。
左移 <<
左移运算符<<将其左侧操作数的值的每位向左移动,移动的位数由其右侧操作数指定。空出来的位用0填充,并且丢弃移出左侧操作数末端的位。在下面例子中,每位向左移动两个位置。
(10001010) << 2
(00101000)
该操作将产生一个新位置,但是不改变其操作数。
1 << 1 = 2;
2 << 1 = 4;
4 << 1 = 8;
8 << 2 = 32
左移一位相当于原值*2.
右移 >>
右移运算符>>将其左侧的操作数的值每位向右移动,移动的位数由其右侧的操作数指定。丢弃移出左侧操作数有段的位。对于unsigned类型,使用0填充左端空出的位。对于有符号类型,结果依赖于机器。空出的位可能用0填充,或者使用符号(最左端)位的副本填充。
//有符号值
(10001010) >> 2
(00100010) //在某些系统上的结果值
(10001010) >> 2
(11100010) //在另一些系统上的解雇
//无符号值
(10001010) >> 2
(00100010) //所有系统上的结果值
用法:移位运算符
number << n number乘以2的n次幂
number >> n 如果number非负,则用number除以2的n次幂
操作符
3/2^4
0000 0011 >> 4(右移4位) --> 0000 0000
3*2^4
0000 0011 << 4(左移4位) --> 0011 0000
1 什么是指针
1.1.1、什么是指针
C语言里,变量存放在内存中,而内存其实就是一组有序字节组成的数组,每个字节有唯一的内存地址。CPU 通过内存寻址对存储在内存中的某个指定数据对象的地址进行定位。这里,数据对象是指存储在内存中的一个指定数据类型的数值或字符串,它们都有一个自己的地址,而指针便是保存这个地址的变量。也就是说:指针是一种保存变量地址的变量。
前面已经提到内存其实就是一组有序字节组成的数组,数组中,每个字节大大小固定,都是 8bit。对这些连续的字节从 0 开始进行编号,每个字节都有唯一的一个编号,这个编号就是内存地址。示意如下图:
这是一个 4GB 的内存,可以存放 2^32 个字节的数据。左侧的连续的十六进制编号就是内存地址,每个内存地址对应一个字节的内存空间。而指针变量保存的就是这个编号,也即内存地址。
1.1.2为什么要使用指针
在C语言中,指针的使用非常广泛,因为使用指针往往可以生成更高效、更紧凑的代码。总的来说,使用指针有如下好处:
1)指针的使用使得不同区域的代码可以轻易的共享内存数据,这样可以使程序更为快速高效;
2)C语言中一些复杂的数据结构往往需要使用指针来构建,如链表、二叉树等;
3)C语言是传值调用,而有些操作传值调用是无法完成的,如通过被调函数修改调用函数的对象,但是这种操作可以由指针来完成,而且并不违背传值调用。
1.1.2 指针表达式
char ch = 'a';
char *cp = &ch;
现在有了两个变量,一个ch(字符类型变量),一个cp(指向字符类型的指针变量)。
这个比较好理解
接下来我们一起看看这个
ch
&ch
cp
&cp
*cp
*cp+1
*(cp+1)
++cp
cp++
*++cp
*cp++
++*cp
(*cp)++
++*++cp
++*cp++
1.1.3如何定义一个指针
-
int p; //这是一个普通的整型变量
-
int p; //首先从P 处开始,先与结合,所以说明P 是一个指针,然后再与int 结合,说明指针所指向的内容的类型为int 型.所以P是一个返回整型数据的指针
-
int p[3]; //首先从P 处开始,先与[]结合,说明P 是一个数组,然后与int 结合,说明数组里的元素是整型的,所以P 是一个由整型数据组成的数组
-
int p[3]; //首先从P 处开始,先与[]结合,因为其优先级比高,所以P 是一个数组,然后再与*结合,说明数组里的元素是指针类型,然后再与int 结合,说明指针所指向的内容的类型是整型的,所以P 是一个由返回整型数据的指针所组成的数组
-
int (p)[3]; //首先从P 处开始,先与结合,说明P 是一个指针然后再与[]结合(与"()"这步可以忽略,只是为了改变优先级),说明指针所指向的内容是一个数组,然后再与int 结合,说明数组里的元素是整型的.所以P 是一个指向由整型数据组成的数组的指针
-
int **p; //首先从P 开始,先与结合,说是P 是一个指针,然后再与结合,说明指针所指向的元素是指针,然后再与int 结合,说明该指针所指向的元素是整型数据.由于二级指针以及更高级的指针极少用在复杂的类型中,所以后面更复杂的类型我们就不考虑多级指针了,最多只考虑一级指针.
-
int p(int); //从P 处起,先与()结合,说明P 是一个函数,然后进入()里分析,说明该函数有一个整型变量的参数,然后再与外面的int 结合,说明函数的返回值是一个整型数据
-
Int (*p)(int); //从P 处开始,先与指针结合,说明P 是一个指针,然后与()结合,说明指针指向的是一个函数,然后再与()里的int 结合,说明函数有一个int 型的参数,再与最外层的int 结合,说明函数的返回类型是整型,所以P 是一个指向有一个整型参数且返回类型为整型的函数的指针
-
int (p(int))[3]; //可以先跳过,不看这个类型,过于复杂从P 开始,先与()结合,说明P 是一个函数,然后进入()里面,与int 结合,说明函数有一个整型变量参数,然后再与外面的结合,说明函数返回的是一个指针,,然后到最外面一层,先与[]结合,说明返回的指针指向的是一个数组,然后再与结合,说明数组里的元素是指针,然后再与int 结合,说明指针指向的内容是整型数据.所以P 是一个参数为一个整数据且返回一个指向由整型指针变量组成的数组的指针变量的函数.
说到这里也就差不多了,我们的任务也就这么多,理解了这几个类型,其它的类型对我们来说也是小菜了,不过我们一般不会用太复杂的类型,那样会大大减小程序的可读性,请慎用,这上面的几种类型已经足够我们用了.
运算符优先级
1.::{作用域}
2.(){函数调用,类型构造:type(exp)},[]{下标},.{成员选择},->{成员选择}
3.++{后置},--{后置},typeid{类型id},explicit_cast{四种类型转换}
4.++{前置},--{前置},~{取反},!{逻辑非},-{一元负},+{一元正},\*{指针指向值},&{取地址},(){老式类型转换},sizeof{对象大小}
5.sizeof{类型或参数包的大小},new{分配内存},delete{释放内存},noexcept{能否抛出异常}
6.->\*{指向成员中的指针},.\*{指向成员中的指针}
7.*,/,%
8.+,-
9.<<,>>
10.<,>,<=,>=
11.==,!=
12.&
1.1.4 指针自身类型
从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。让我们看看例一中各个指针的类型:
(1)int * ptr;//指针的类型是int *
(2)char * ptr;//指针的类型是char *
(3)int ** ptr;//指针的类型是int **
(4)int( * ptr)[3];//指针的类型是int(*)[3]
(5)int * ( * ptr)[4];//指针的类型是int*(*)[4]
怎么样?找出指针的类型的方法是不是很简单?
1.1.5 指针所指向的类型 (重要)
当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。 从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。例如:
(1)int * ptr; //指针所指向的类型是int
(2)char * ptr; //指针所指向的的类型是char
(3)int * * ptr; //指针所指向的的类型是int*
(4)int( * ptr )[3]; //指针所指向的的类型是int()[3]
(5)int * ( * ptr)[4]; //指针所指向的的类型是int *()[4]
在指针的算术运算中,指针所指向的类型有很大的作用。他决定我们昨天所讲的步长
指针的类型(即指针本身的类型)和指针所指向的类型是两个概念。当你对C 越来越熟悉时,你会发现,把与指针搅和在一起的"类型"这个概念分成"指针的类型"和"指针所指向的类型"两个概念,是精通指针的关键点之一。
每遇到一个指针,都应该问问:这个指针的类型是什么?指针指的类型是什么?该指针指向了哪里?(重点注意)
1.1.6 指针的算术运算
指针可以加上或减去一个整数。指针的这种运算的意义和通常的数值的加减运算的意义是不一样的,以单元为单位。例如:
char a[20] = {'a', 'b', 'c', 'd','e'};
int *ptr=(int *)a; //强制类型转换并不会改变a 的类型
ptr++;
printf("char %c\n", *a);
printf("char %c\n", *ptr)
在上例中,指针ptr 的类型是int*,它指向的类型是int,它被初始化为指向整型变量a。接下来的第3句中,指针ptr被加了1,编译器是这样处理的:它把指针ptr 的值加上了sizeof(int),
int 占4 个字节。由于地址是用字节做单位的,故ptr 所指向的地址由原来的变量a 的地址向高地址方向增加了4 个字节。由于char 类型的长度是一个字节,所以,原来ptr 是指向数组a 的第0 号单元开始的四个字节,此时指向了数组a 中从第4 号单元开始的四个字节。我们可以用一个指针和一个循环来遍历一个数组,看例子:
int array[4]={0};
int *ptr=array;
//printf("ptr : 0x%x\n", ptr );
for(int i=0;i<4;i++)
{
(*ptr)++;
ptr++;
}
for (int i = 0; i < 4; ++i) {
printf("value: %d\n", *(array+i));
}
//printf("ptr : 0x%x\n", ptr );
//printf("ptr : %d\n", *(ptr-1) );
return 0;
}
这个例子将整型数组中各个单元的值加1。由于每次循环都将指针ptr加1 个单元,所以每次循环都能访问数组的下一个单元。
char a[20]="You_are_a_girl";
int *ptr=(int *)a;
printf("* ptr addr %p\n", ptr);
ptr+=1;
printf("* ptr addr %p\n", ptr);
printf("* ptr=%c\n", *ptr);
在这个例子中,ptr 被加上了1,编译器是这样处理的:将指针ptr 的值加上1 乘sizeof(int), 由于地址的单位是字节,故现在的ptr 所指向的地址比起加4 后的ptr 所指向的地址来说,向高地址方向移动了4 个字节。
假设 加的不是1 而是 加的是 5 呢 ptr+=5;
没加5 前的ptr 指向数组a 的第0 号单元开始的四个字节,加5 后,ptr 已经指向了数组a 的合法范围之外了。虽然这种情况在应用上会出问题,但在语法上却是可以的。这也体现出了指针的灵活性。如果上例中,ptr 是被减去5,那么处理过程大同小异,只不过ptr 的值是被减去5 乘sizeof(int),新的ptr 指向的地址将比原来的ptr 所指向的地址向低地址方向移动了20 个字节。
下面请 再举一个例子
char a[20]="You_are_a_girl";
char *p=a;
char **ptr=&p;
printf("ptr=0x%x\n",ptr);
printf("p=%c\n",*p);
printf("**ptr=%c \n",**ptr);
printf("beafor ptr= 0x%x\n", ptr);
ptr++;
printf("after ptr= 0x%x\n", ptr);
printf("ptr= %c\n",**ptr);
- 误区一、输出答案为Y 和o 误解:ptr 是一个char 的二级指针,当执行ptr++;时,会使指针加一个sizeof(char),所以输出如上结果,这个可能只是少部分人的结果.
- **误区二、**输出答案为Y 和a误解:ptr 指向的是一个char *类型,当执行ptr++;时,会使指针加一个sizeof(char *)(有可能会有人认为这个值为1,那就会得到误区一的答案,这个值应该是4,参考前面内容), 即&p+4; 那进行一次取值运算不就指向数组中的第五个元素了吗?那输出的结果不就是数组中第五个元素了吗?答案是否定的.
- 正解: ptr 的类型是char ,指向的类型是一个char * 类型,该指向的地址就是p的地址(&p),当执行ptr++;时,会使指针加一个sizeof(char),即&p+4;那(&p+4)指向哪呢,这个你去问上帝吧,或者他会告诉你在哪?所以最后的输出会是一个随机的值,或许是一个非法操作.
1.1.7指针总结:
一个指针ptr 加(减)一个整数n 后,
- 结果是一个新的指针ptr_new,ptr_new 的类型和ptr_old 的类型相同,
- ptr_new 所指向的类型和ptr_old所指向的类型也相同。
- ptr_new 的值将比ptr_old 的值增加(减少)了n 乘sizeof(ptrold 所指向的类型)个字节。
就是说,ptr_new 所指向的内存区将比ptr_old 所指向的内存区向高(低)地址方向移动了n 乘sizeof(ptr_old 所指向的类型)个字节。
指针和指针进行加减:两个指针不能进行加法运算,这是非法操作,因为进行加法后,得到的结果指向一个不知所向的地方,而且毫无意义。
对自身的指针可以进行加减1操作, 一般用在数组方面,不多说了。
2 内存分配
C语言的标准内存分配函数:malloc,calloc,realloc,free等。
区别:
malloc与calloc的区别为1个size与n个size大小内存的区别:
使用方式
malloc调用形式为(类型 ) **malloc(size):**在内存的动态存储区中分配一块长度为“size”字节的连续区域,返回该区域的首地址。
calloc调用形式为(类型 ) **calloc(n,size):**在内存的动态存储区中分配n块长度为“size”字节的连续区域,返回首地址。
realloc调用形式为(类型) **realloc(*ptr,size):**将ptr内存大小增大到size。
free的调用形式为(类型) **free(void *ptr):**释放ptr所指向的一块内存空间。
2.1.1 共同点就是:
- 都为了分配存储空间,
- 它们返回的是 void * 类型,也就是说如果我们要为int或者其他类型的数据分配空间必须显式强制转换;
2.1.2 不同点是:
- malloc一个形参,因此如果是数组,必须由我们计算需要的字节总数作为形参传递 用malloc只分配空间不初始化,也就是依然保留着这段内存里的数据,
- calloc 2个形参 ,因此如果是数组,需要传递个数和数据类型 而calloc则进行了初始化,calloc分配的空间全部初始化为0,这样就避免了可能的一些数据错误。
3 内存管理机制
内存资源是非常有限的。尤其对于移动端开发者来说,硬件资源的限制使得其在程序设计中首要考虑的问题就是如何有效地管理内存资源。本文是作者在学习C语言内存管理的过程中做的一个总结。
3.1 变量概念:
- 全局变量(外部变量):出现在代码块{}之外的变量就是全局变量。
- 局部变量(自动变量):一般情况下,代码块{}内部定义的变量就是自动变量,也可使用auto显示定义。
- 静态变量:是指内存位置在程序执行期间一直不改变的变量,用关键字static修饰。 代码块内部的静态变量只能被这个代码块内部访问,代码块外部的静态变量只能被定义这个变量的文件访问。
3.1.2 extern关键字:
1、引用同一个文件中的变量; 2、引用另一个文件中的变量; 3、引用另一个文件中的函数。
注意:C语言中函数默认都是全局的,可以使用static关键字将函数声明为静态函数(只能被定义这个函数的文件访问的函数)。
3.1.4 程序执行流程:
代码区:
程序被操作系统加载到内存的时候,所有的可执行代码(程序代码指令、常量字符串等)都加载到代码区,这块内存在程序运行期间是不变的。代码区是平行的,里面装的就是一堆指令,在程序运行期间是不能改变的。函数也是代码的一部分,故函数都被放在代码区,包括main函数。
静态区
静态区存放程序中所有的全局变量和静态变量。
栈区 栈(stack)是一种先进后出的内存结构,所有的自动变量、函数形参都存储在栈中,这个动作由编译器自动完成,我们写程序时不需要考虑。栈区在程序运行期间是可以随时修改的。当一个自动变量超出其作用域时,自动从栈中弹出。
每个线程都有自己专属的栈; 栈的最大尺寸固定,超出则引起栈溢出; 变量离开作用域后栈上的内存会自动释放。
int main(int argc, char* argv[])
{
char array_char[1024*1024*1024] = {0};
array_char[0] = 'a';
printf("%s", array_char);
getchar();
}1234567
栈溢出怎么办呢?就该堆出场了。
堆(heap)和栈一样,也是一种在程序运行过程中可以随时修改的内存区域,但没有栈那样先进后出的顺序。更重要的是堆是一个大容器,它的容量要远远大于栈,这可以解决内存溢出困难。一般比较复杂的数据类型都是放在堆中。但是在C语言中,堆内存空间的申请和释放需要手动通过代码来完成。
那堆内存如何使用?
malloc函数用来在堆中分配指定大小的内存,单位为字节(Byte),函数返回void *指针;free负责在堆中释放malloc分配的内存。
#include <stdlib.h>
#include<stdio.h>
#include <string.h>
void print_array(char *p, char n)
{
int i = 0;
for (i = 0; i < n; i++)
{
printf("p[%d] = %d\n", i, p[i]);
}
}
int main(int argc, char* argv[])
{
char *p = (char *)malloc(1024 * 1024 * 1024);//在堆中申请了内存
memset(p, 'a', sizeof(int)* 10);//初始化内存
int i = 0;
for (i = 0; i < 10; i++)
{
p[i] = i + 65;
}
print_array(p, 10);
free(p);//释放申请的堆内存
getchar();
}1234567891011121314151617181920212223242526
这样就解决了刚才栈溢出问题。堆的容量有多大?理论上讲,它可以使用除了系统占用内存空间之外的所有空间。实际上比这要小些,比如我们平时会打开诸如QQ、浏览器之类的软件,但这在一般情况下足够用了。不能将一个栈变量的地址通过函数的返回值返回,如果我们需要返回一个函数内定义的变量的地址该怎么办?可以这样做:
int *getx()
{
int *p = (int *)malloc(sizeof(int));//申请了一个堆空间
return p;
}
int main(int argc, char* argv[])
{
int *pp = getx();
*pp = 10;
free(pp);
return 0;
}
//类似创建链表时,新增一个节点。1234567891011121314
可以通过函数返回一个堆地址,但记得一定用通过free函数释放申请的堆内存空间。
分析:
main函数和UpdateCounter为代码的一部分,故存放在代码区
数组a默认为全局变量,故存放在静态区
main函数中的”char *b = NULL”定义了自动变量b(variable),故其存放在栈区
接着malloc向堆申请了部分内存空间,故这段空间在堆区
需要注意以下几点:
栈是从高地址向低地址方向增长; 在C语言中,函数参数的入栈顺序是从右到左,因此UpdateCounter函数的3个参数入栈顺序是a1、c、b; C语言中形参和实参之间是值传递,UpdateCounter函数里的参数a[1]、c、b与静态区的a[1]、c、b不是同一个;
3.1.5 内存管理的目的
学习内存管理就是为了知道日后怎么样在合适的时候管理我们的内存。那么问题来了?什么时候用堆什么时候用栈呢?一般遵循以下三个原则:
- 如果明确知道数据占用多少内存,那么数据量较小时用栈,较大时用堆;
- 如果不知道数据量大小(可能需要占用较大内存),最好用堆(因为这样保险些);
- 如果需要动态创建数组,则用堆。
创建动态数组:
//动态创建数组
int main()
{
int i;
scanf("%d", &i);
int *array = (int *)malloc(sizeof(int) * i);
//...//这里对动态创建的数组做其他操作
free(array);
return 0;
}12345678910
操作系统在管理内存时,最小单位不是字节,而是内存页(32位操作系统的内存页一般是4K)。比如,初次申请1K内存,操作系统会分配1个内存页,也就是4K内存。4K是一个折中的选择,因为:内存页越大,内存浪费越多,但操作系统内存调度效率高,不用频繁分配和释放内存;内存页越小,内存浪费越少,但操作系统内存调度效率低,需要频繁分配和释放内存。
4 异常指针
空悬指针是这样一种指针:指针正常初始化,曾指向过一个正常的对象,但是对象销毁了,该指针未置空,就成了悬空指针。
野指针 是这样一种指针:未初始化的指针,其指针内容为一个垃圾数。 (一般我们定义一个指针时会初始化为NULL或者直接指向所要指向的变量地址,但是如果我们没有指向NULL或者变量地址就对指针进行使用,则指针指向的内存地址是随机的)。存在野指针是一个严重的错误。
int main() {
int *p; // 指针未初始化,此时 p 为野指针
int *pi = nullptr;
{
int i = 6;
pi = &i; // 此时 pi 指向一个正常的地址
*pi = 8; // ok
}
*pi = 6; // 由于 pi 指向的变量 i 已经销毁,此时 pi 即成了悬空指针
return 0;
}
空指针与NULL指针
4.1.1 什么是空指针
如果 p 是一个指针变量,则 p = 0; p = 0L; p = '\0'; p = 3 - 3; p = 0 * 17; 中的任何一种赋值操作之后(对于 C 来说还可以是 p = (void*)0;), p 都成为一个空指针,由系统保证空指针不指向任何实际的对象或者函数。反过来说,任何对象或者函数的地址都不可能是空指针。(比如这里的**(void*)0就是一个空指针**
4.1.2什么是NULL指针
NULL 是一个标准规定的宏定义,用来表示空指针常量。因此,除了上面的各种赋值方式之外,还可以用 p = NULL; 来使 p 成为一个空指针。与上一钟情况相似,只不过是空指针的一种例子
4.1.3为什么通过空指针读写的时候就会出现异常?
NULL指针分配的分区:其范围是从 0x00000000到0x0000FFFF。这段空间是空闲的,对于空闲的空间而言,没有相应的物理存储器与之相对应,所以对这段空间来说,任何读写操作都是会引起异常的。空指针是程序无论在何时都没有物理存储器与之对应的地址。为了保障“无论何时”这个条件,需要人为划分一个空指针的区域,固有上面NULL指针分区。
C语言基础 指针详解02
void test01() {
int p;
int *p1;//整型指针
int p2[2];//整型数组
int *p4[4];//指针数组
int (*p5)[4];//数组指针
//
int *p6;//指针类型是int * 指向类型 int
char *p7;
int **p8;
int(*p9)[3];
int *p11 = p4;
printf("p11 : %p\n", p11);
p11++;
printf("p11++ : %p\n", p11);
int *p13;//int 4个字节 sizeof(指向的类型) 去掉变量名和*
int **p14; //指向类型int *
}
int *p = NULL;
*p = 0;(错误)
void test01() {
char ch = 'a';
char *cp = &ch;
printf("cp: %p\n", cp);
printf("ch: %p\n", &ch);
char *name = &ch;
printf("name: %c\n", *name);
}
result:
cp: 000000000022FE0F
ch: 000000000022FE0F
name: a
//右值
char *cp1 = cp;
printf("cp: %p\n", cp);
printf("cp1: %p\n", cp1);
//左值
cp = NULL;
printf("cp: %p\n", cp);
printf("cp1: %p\n", cp1);
result:
cp: 000000000022FE07
cp1: 000000000022FE07
cp: 0000000000000000
cp1: 000000000022FE07
#include <stdio.h>
#include <malloc.h>
void test01() {
int *p = NULL;
printf("p: %p\n", p);
malloc(16);
}
result:
p: 0000000000000000
int *p;
printf("p: %p\n", p);
result:
p: 0000000000000008
int *p1;
*p1 = 10;(错误)
数组指针、指针数组
void test() {
int a = 100;
int *p = &a;
printf("p 获取的a的地址: 0x%p\n", p);
printf("p 获取的a的地址中存的值: %d\n", *p);
printf("p 本身的地址: 0x%p\n", &p);
}
result:
p 获取的a的地址: 0x000000000022FE1C
p 获取的a的地址中存的值: 100
p 本身的地址: 0x000000000022FE10
void test() {
int a = 0;
//p++ 指针步长 n*sizeof(指针指向类型)
int *p = &a;//指针自身类型 int * ,指针指向类型 int
int **temp = &p;//指针自身类型 int ** ,指针指向类型 int *
printf("int * : %d\n", sizeof(int *));
printf("temp: %p\n", temp);
temp++;
printf("temp++: %p\n", temp);
}
result:
int * : 8
temp: 000000000022FE08
temp++: 000000000022FE10
VIP07-2020.11.05-c++基础01(C++入门与输入流和输出流) -DAVID
c的写法:
main.cpp
#include <iostream>
struct Student {
//结构体包含的成员变量
char *name;
int age;
float score;
};
void display(struct Student stu) {
printf("name = %s age = %d score = %f\n", stu.name, stu.age, stu.score);
}
int main() {
struct Student stu1;
//为结构体的成员变量赋值
stu1.name = "xiaoming";
stu1.age = 18;
stu1.score = 93.5;
//调用函数
display(stu1);
return 0;
}
main1.cpp
#include <iostream>
struct Student {
//结构体包含的成员变量
char *name;
int age;
float score;
};
void display(struct Student stu) {
printf("name = %s age = %d score = %f\n", stu.name, stu.age, stu.score);
}
int main1() {
std::cout << "Hello, World!" << std::endl;
struct Student stu1;
//为结构体的成员变量赋值
stu1.name = "xiaoming";
stu1.age = 15;
stu1.score = 92.5;
return 0;
}
编译报错:同时存在2个display方法
CMakeFiles\vip07_201105.dir/objects.a(main1.cpp.obj): In function `display(Student)':
D:/code/media code/media c++/cplus/vip07 201105/main1.cpp:10: multiple definition of `display(Student)'
CMakeFiles\vip07_201105.dir/objects.a(main.cpp.obj):D:/code/media code/media c++/cplus/vip07 201105/main.cpp:10: first defined here
collect2.exe: error: ld returned 1 exit status
mingw32-make.exe[3]: *** [CMakeFiles\vip07_201105.dir\build.make:100: vip07_201105.exe] Error 1
mingw32-make.exe[2]: *** [CMakeFiles\Makefile2:75: CMakeFiles/vip07_201105.dir/all] Error 2
mingw32-make.exe[1]: *** [CMakeFiles\Makefile2:82: CMakeFiles/vip07_201105.dir/rule] Error 2
mingw32-make.exe: *** [Makefile:117: vip07_201105] Error 2
c++的写法:
#include <iostream>
class Student {
public:
char *name;
int age;
float score;
void say() {
printf("name = %s age = %d score = %f\n", name, age, score);
}
};
int main() {
class Student stu1;
//为类的成员变量赋值
stu1.name = "xiaoming";
stu1.age = 15;
stu1.score = 92.5;
stu1.say();
return 0;
}
命名空间-namespace
#include <iostream>
//包名
namespace namespaceA {
int a = 10;
}
int main() {
//访问a
int a = namespaceA::a;
return 0;
}
多个命名空间
#include <iostream>
//包名
namespace namespaceA {
int a = 10;
}
namespace namespaceB {
int a = 10;
}
int main() {
int a = namespaceA::a;
int bA = namespaceB::a;
return 0;
}
//多个命名空间容易造成混乱
#include <iostream>
namespace namespaceA {
int a = 10;
}
namespace namespaceB {
int a = 10;
}
int main() {
using namespace namespaceA;
using namespace namespaceB;
int c = a;//(报错,多个命名空间不知选哪个,此时用哪个就只加这个)
return 0;
}
#include <iostream>
//包名
namespace namespaceA{
int a = 10;
}
namespace namespaceB{
int a = 10;
namespace namespaceC {
struct Teacher {
int age = 20;
};
}
}
int main() {
//int a = namespaceA::a;
//int b = namespaceB::a;
using namespace namespaceA;
using namespace namespaceB;
//A,B存在相同变量a时
int c = namespaceB::a;
//namespace里面嵌套namespace
//写法1
namespaceB::namespaceC::Teacher teacher;
//写法2
using namespace namespaceB::namespaceC;
//写法3
Teacher teacher1;
return 0;
}
输出流-cout
#include <iostream>
using namespace std;
int main() {
//c \n换行
printf("hello c!\n");
//cpp endl换行 << 变量 ,等于java中 + 变量
std::cout << "hello cpp!" << std::endl;
cout << "hello cpp!" << endl;
cout << "this is "
<< "c++ "
<< "program"
<< endl;
return 0;
}
输入流-cin
#include <iostream>
using namespace std;
int main() {
int a, b, c, d;
cin >> a >> b >> c;
cout << a << b << c << endl;
return 0;
}
引用: 就是某一变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样
引用的好处:1、不会给引用任何内存空间
#include <iostream>
//时间:0:57:50
int main() {
std::cout << "hello cpp!" << std::endl;
int a = 10;
int &b = a;//引用
printf("a: %d\n", a);
printf("b: %d\n", b);
printf("a: %p\n", &a);
printf("b: %p\n", &b);
b = 100;//a,b 同时改变了
printf("a: %d\n", a);
printf("b: %d\n", b);
return 0;
}
引用交换
#include <iostream>
//时间:0:57:50 交换
void swap1(int p1, int p2) {
int p = p1;
p1 = p2;
p2 = p;
}
//指针交换
void swap2(int *p1, int *p2) {
int p = *p1;
*p1 = *p2;
*p2 = p;
}
//纯指针的弊端:不断的取址 不断的找到指向的内容
void swap3(int &p1, int &p2) {
int p = p1;//相当于p1 = *p1, p就是int变量
p1 = p2;
p2 = p;
}
int main() {
int a = 28, b = 40;
std::cout << "a = " << a << " ,b = " << b << std::endl;
swap1(a, b);
std::cout << "a = " << a << " ,b = " << b << std::endl;
swap2(&a, &b);
std::cout << "a = " << a << " ,b = " << b << std::endl;
swap3(a, b);
std::cout << "a = " << a << " ,b = " << b << std::endl;
return 0;
}
常量引用
#include <iostream>
//常量引用
int main() {
int a = 1;
int &b = a;
b = 2;
std::cout << "a = " << a << " ,b = " << b << std::endl;
int c = 1;
const int &d = c;
std::cout << "c = " << c << " ,d = " << d << std::endl;
return 0;
}
引用在函数里
#include <iostream>
using namespace std;
float fn1(float r) {
float temp = (float) (r * r * 3.14);
return temp;
}
float &fn2(float r) {
float temp = (float) (r * r * 3.14);
float &b = temp;
return b;
}
float &fn3(float r) {
float temp = (float) (r * r * 3.14);
return temp;//打印不出来
}
//fn3修改为
float temp;
float &fn4(float r) {
temp = (float) (r * r * 3.14);
return temp;
}
//引用在函数里
int main() {
float a = fn1(10.0);
float b = fn2(10.0);
//float c = fn3(10.0);
float c = fn4(100);
std::cout << "a = " << a << " ,b = " << b << " ,c = " << c << std::endl;
return 0;
}
引用作为数组
#include <iostream>
using namespace std;
int error = -1;
int vals[10];
int &put(int n) {
//int vals[10];//函数内引用会被销毁报错,此时需要定义全局变量
if (n >= 0 && n <= 9) {
return vals[n];
} else {
cout << "error";
return error;
}
}
int main() {
put(0) = 10;//第0个元素,引用赋值为10,相当于以put(0)函数值作为左值,等价于vals[0]=10
return 0;
}
内联函数
#include <iostream>
using namespace std;
#define ADD(x, y) (x+y)
//内联函数
inline int Add(int x, int y) {
return x + y;
}
int main() {
int res = Add(10, 20) * 10;
cout << "res: " << res << endl;
return 0;
}
重载函数
#include <iostream>
using namespace std;
#define ADD(x, y) (x+y)
//函数重载
void test(int x, int y = 10) {
cout << "test -> x: " << x << endl;
}
//默认参数
void test2(int a, int b, int c = 10, int d = 20) {
cout << "test -> a: " << a << " ,b: " << b << endl;
}
//占位参数 int=0,没有变量名
void test3(int a, int b, int= 0) {
}
int main() {
test(10);
test(20, 30);
test2(1, 2, 3);
return 0;
}
int 和int & 不算重载
#include <iostream>
using namespace std;
//函数重载 int 和int & 不算重载
void test(int a) {
cout << "test -> a: " << a << endl;
}
//引用重载
void test(int &a) {
cout << "test -> &a: " << a << endl;
}
void test(int a, int b) {
cout << "a: " << a << " ,b: " << b << endl;
}
int main() {
test(10);
int a = 10;
int &b = a;
test(b);
return 0;
}
指针和引用不冲突
#include <iostream>
using namespace std;
//函数重载
//指针
void test(int *a) {
cout << "test -> *a: " << a << endl;
}
//引用
void test(int &a) {
cout << "test -> &a: " << a << endl;
}
int main() {
int a = 10;
test(&a);
return 0;
}
时间31:24 常量指针与指针重载
字符串
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void test01() {
char str1[] = "abcdef";
char *str2 = "adbcd";
printf("%s\n", str2);
while (*str2 != '\0') {
printf("%c ", *str2++);
}
printf("xxx %d\n", strlen(str2));
}
int main() {
test01();
return 0;
}