文章目录
指针和动态内存分配
指针是C语言的基本概念,C语言中指针无处不在。实际上,每种数据类型,都有相应的指向T的指针类型。
指针类型变量存放的值,实际上就是内存地址。指针类型有两个最基本的操作:
&:取地址操作
*:去引用 (间接引用)操作
引用&
首先,&不是地址运算符,而是类型标识符的一种,就像*也不是指针运算符一样。
就像char* 意为指向char的指针一样,int& 意为指向int 的引用。
栗子来一颗:
int a;
int &at = a;
//上述声明允许将at和a互换,它们指向相同的值和内存单元,就像连体婴一样。
上面这个栗子其实很有内涵在里面
我为什么不写成下面这个形式呢?
int a;
int &at;
at = a;
在指针中是可以的,但是&不允许,&必须在声明时将其初始化。
引用经常被用作函数参数,使得函数中的变量名成为调用程序中变量的别名。这种调用方法我一直搞得晕晕的,正好这次一次性根除。这种传递参数的方法称为按引用传递。按引用传递允许被调用函数能够访问调用函数中的变量。这是C++相比C的一个超越。
来个经典的栗子:
void swap_a(int &a,int &b)
{
int temp;
temp = a;
a = b;
b = temp;
}
//顺便来个指针的
void swap_b(int *a,int *b)
{
int temp;
temp = *a; //a,b是指针,*a,*b才是int
*a = *b;
*b = temp;
}
int main()
{
int a = 1;
int b = 2;
int c = 3;
int d = 4;
swap_a(a,b); //看仔细咯,这个是引用调用
swap_b(&a,&b); //看仔细咯,这个是指针调用
//如果理解不了,这样理解:参数中的*和&只是走个过场,告诉人家那个参数是什么类型的
//调用函数时的参数是a,不是*a,也不是&a
//所以&a传的这个a是一个int类型,而*a的这个a就是指针,地址,所以要取地址传给它
//虽然我语文不好,但是都讲到这份上了那应该是可以理解了
return 0;
}
如果你的意图是让函数使用传给它的信息,又不想把这些信息进行改动,那么应该使用const。
将引用参数声明为const数据的好处有这些:
防止无意中被修改。
使用const参数可以兼容非const传参。
将引用用于结构
C++引入引用主要就是为了和结构和类。
它还通过让函数返回指向结构的引用而增添了一个有趣的特点,这与返回结构有所不同。
//代码太长,放段伪代码吧
struct Str //不知道什么是结构体不急,稍后就会有
{
};
Str& test(Str &a,const Str &b)
{
//从b中取值,对a进行填充
return a;//其实可以做void类型,没必要多此一举
}
int main()
{
Str a,b,c;
//b是有初值的,这是伪代码
c = test(a,b);
return 0;
}
如果test函数返回一个结构,而不是指向结构的引用,相当于把整个结构体复制到一个临时位置,再将这个拷贝复制给c,但是现在返回值为引用,将直接将a复制到c,效率更高。
返回引用时最重要的一点是:应避免返回函数终止时将不再存在的内存单元的引用。
下面是一个反面教材:
Str& test(const Str &d)
{
Str &e;
···
return e;
}
何时使用引用参数?
程序员能够修改调用函数中的数据对象。
通过传递引用而不是整个数据对象,可以提高程序的运行速度。
指针
指针和const
将const用于指针有一些很微妙的地方。
可以用两种不同的方式将const关键字用于指针。
int age = 20; const int * pt = &age;
//该声明指出,pt指向一个const int,因此不能使用pt来修改这个值。
//现在来看一个很微妙的问题:其实age并不是一个常量,只是对于pt来说,它是一个常量。
//就是说age可以改,只不过不能用pt来改而已。
注意点:不允许将常量数据赋值给非常量指针,个中理由就不用多解释了吧。
const int age = 20; int * pt = &age;
int sloth = 80; int * const finger = &sloth;
// 这种声明格式使得这个指针只能指向sloth,不过可以通过这个指针修改sloth的值。
通过指针返回字符串的函数
现在,假设需要一个返回字符串的函数,是的,函数无法返回一个字符串,但是可以返回字符串的地址,这样效率更高。
void test(char *rc)
{
···
memset(rc,字符串);
···
}
相当于是使用回调函数,我个人比较喜欢这一套模式。
通过指针返回结构
具体操作参考第二点。
当然,这里还有另外的应用场景:
void test2(const JieGouTi1 *a,JieGouTi2 *b)
{
//将a中的某些值赋值给b
}
//这里有一个注意点,传进去赋值的结构体指针最好用const.
函数指针
关于为什么要使用函数指针,我的理解还不是很深刻,毕竟功力不足。但是我知道那些回调函数都是用函数指针的,所以对函数指针必须要理解好。
这叫啥,“但行好事,莫问为啥”。
函数指针完成任务的流程是这样的:
获取函数的地址
声明一个函数指针
使用函数指针来调用函数
获取函数地址
获取函数地址那是比较简单的事,如果说 void Hanshu();这是一个函数,那么它的地址就是 Hanshu。
如果函数Hanshubaba();要调用这个函数,是这样的:Hanshubaba(Hanshu);
切记不能写成:Hanshubaba(Hanshu());
声明函数指针
假设现在有这么一个函数:int test3(void *arg); //这个arg参数,回调函数里面用,要解释有点长。
现在要将之改成函数指针形式:int (*test3)(void *arg);
首先,将test3更换成(*test3),因此,(*test3)也是函数,那么test3就是函数指针。
为声明优先级,需要将 *test3 括号起来。
函数指针用武之地
如果你非要我说函数指针存在的意义,那我也真不好给你扯个所以然出来,那我就,举几个用得到的地方吧:
自定义排序/搜索
不同的模式(如策略,观察者)
回调
关于指针的一些思考
前面说到,将指针作为参数传入,在函数内部对指针进行修改,函数结束后指针的修改将被保留。
因为指针传参代表着地址传参。
解惑:如何让对指针参数的修改不被保存。
看个栗子:
class B {
char* b;
public:
B() {
b = new char[5];
strcpy(b,"aaaa");
}
char* get_b() { return b; }
};
class A {
private:
char* a;
public:
A(B* temp) { a = temp->get_b(); };
void set_A() {
strcpy(a, "kkkk"); //顶替掉了
}
};
int main() {
B* b = new B();
A* a = new A(b);
a->set_A();
cout << b->get_b() << endl;
return 0;
}
结局打印出来的 b,就是“kkkk”。
那为什么会这样?前面解释过了,a、b都是对内存地址的映射,对a进行修改,就是对地址上的数据进行修改,而b只不过是地址的一个映射而已,读取b,就是读取地址上的东西,那本质已经被改了,读出来的东西自然不一样。
再看个例子:
void Del (POINT_T * the_head, int index)
{
POINT_T *pFree=NULL;
POINT_T *pNode=the_head;
int flag=0;
while (pNode->next!=NULL)
{
if(flag==index-1)
{
pFree=pNode->next; //再指向数据域就爆了
pNode->next=pNode->next->next;
free(pFree->pData);
free(pFree);
break;
}
pNode=pNode->next;
flag++;
}
}
这是链表的一个例子,那可能会纳闷儿,为什么对 pNode执行了 pNode=pNode->next;
操作,而the_head
却没有跟着变呢?
原因很简单,pNode->next也是一个映射地址,这句话的意思就是用一个新的地址映射,顶替掉那个旧的,使得指针pNode指向一块新的地址,和the_head失去联系。
结构体
结构是 C 编程中一种用户自定义的可用的数据类型,它允许我们存储不同类型的数据项。
struct tag { // 定义一个结构体,名字叫tag
member-list // 结构体成员变量
member-list
member-list
...
} variable-list ; // 结构体的简称
如果有简称时,初始化结构体对象是这样的:
variable-list vl;
variable-list *vl2;
如果没有简称时,初始化结构体对象是这样的:
struct tag t;
struct tag *t2;
//就是要带上‘struct’
在一般情况下,tag、variable-list 这 2 部分至少要出现 1 个。
调试
调试呢,是我们解决代码运行过程中突然暴雷的一个很好的手段,如果代码量一大的时候,凭肉眼想找到bug太难了。
但是如果我们对程序的运行流程应该是有一定的设想的吧,就是不知道实际它有没有阳奉阴违。
调试,就是放慢程序运行的速度,让我们看清楚它内部是如何运行的。
1.选择需要检查或暂停运行的行,如下图红色方框前
2.点击Windows调试器(或者F5)
3、让程序一步步执行,点击单步执行(F10)、进入函数(F11)、跳出函数(shift+F11)、下一个断点(F5)
是可以在代码中打多个断点的。
(每个人的界面排版不一定一样,所以建议使用快捷键法)
程序执行时,可以看到每个变量的状态
简单调试就介绍到这里,大家可以先练习一下。
链表
链表在C语言的数据结构中的地位可不低。后面很多的数据结构,特别是树,都是基于链表发展的。
所以学好链表,后面的结构才有看的必要。
初识链表
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。
链表有很多种不同的类型:单向链表,双向链表以及循环链表。
单链表
单链表实现
话不多说啊,这里我只想直接放代码:
#include <stdio.h> //初学者,C语言开手
#include <conio.h>
#include <stdlib.h>
#include <memory.h>
#include <assert.h>
//节点数据结构体
typedef struct test
{
char name[12]; //名字
char pwd[8]; //密码
int number; //编号
int flag; //区分管理员和用户 // 0 超级管理员 1 管理员 2 普通用户 3 屏蔽用户
int money; //仅用户有存款,初始500
} TEST_T;
//如果不多来一个数据域,怎么能体现出通用链表的优势
typedef struct reported
{
int amount;//交易金额
int rflag; //交易方式 1、存款 2、取款 3、转账转出 4、转账转入
int lastmoney;//余额
int lastmoney2;//收款者的余额
int number1;//付款账户
int number2;//入款账户
char time[12];//操作时间
} REPORT_T;
//节点描述结构体
typedef struct point
{
void *pData; //指向数据域
struct point *next; //指向下一个节点
} POINT_T;
POINT_T * head ;
extern POINT_T * head;
这还是个通用链表的头呢!!!
//创建结点
POINT_T * creat(void *data ) //创建一个属于结构体point的函数,
//传入结构体test的指针便可以用以操作test变量,
{ //并返回一个point的指针用以操作point函数
POINT_T *p=NULL;
p=(POINT_T *)malloc(sizeof(POINT_T));
if(p==NULL)
{
printf("申请内存失败");
exit(-1);
}
memset(p,0,sizeof(POINT_T));
p->pData=data;
p->next=NULL; //处理干净身后事
return p;
}
//新增节点
void add(POINT_T * the_head,void *data ) //这里的data不会和上面那个冲突吗?
{
POINT_T * pNode=the_head; //把头留下
POINT_T *ls=creat(data);
//后面再接上一个
while (pNode->next != NULL) //遍历链表,找到最后一个节点
{
pNode=pNode->next;
}
pNode->next=ls; //ls 临时
}
//删除节点
void del(POINT_T * the_head, int index)
{
POINT_T *pFree=NULL; //用来删除
POINT_T *pNode=the_head;
int flag=0;
while (pNode->next!=NULL)
{
if(flag==index-1)
{
pFree=pNode->next; //再指向数据域就爆了
pNode->next=pNode->next->next; //这里要无缝衔接
free(pFree->pData); //先释放数据
free(pFree); //释放指针
break;
}
pNode=pNode->next;
flag++;
}
}
//计算节点数
int Count(POINT_T * the_head)
{
int count=0;
POINT_T *pNode1=the_head;
while (pNode1->next!=NULL)
{
pNode1=pNode1->next;
count++;
}
return count;
}
//查找固定节点数据
POINT_T * find(POINT_T *the_head,int index)
{
int f=0;
POINT_T *pNode=NULL;
int count=0;
pNode=the_head;
count=Count(the_head);
if(count<index)
printf("find nothing");
while(pNode->next!=NULL)
{
if(index==f)
return pNode;
pNode=pNode->next;
f++;
}
}
文件读写
在我刚接触这一个知识点的时候,我是非常害怕的。不知道各位是什么心情,我那时候只是个培训了一个月的菜鸟。
但是呢,随着学习的深入,我现在反倒觉得,文件读写,比前面的链表操作要简单的多,甚至于比那个输入输出控制函数都要简单。
使用 fopen( ) 函数来创建一个新的文件或者打开一个已有的文件:
FILE *fopen( const char * filename, const char * mode ); //返回值是一个文件句柄
第一个参数是文件名,第二个参数是打开权限:
模式 | 描述 |
---|---|
r | 打开一个已有的文本文件,允许读取文件。 |
w | 打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。程序会从文件的开头写入内容。如果文件存在,会造成覆盖。 |
a | 打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。如果存在,程序会在已有的文件内容中追加内容。 |
r+ | 打开一个文本文件,允许读写文件。 |
w+ | 打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件。 |
a+ | 打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。 |
二进制的话,加一个b,碧如:wb
关闭文件的话:
int fclose( FILE *fp );
写入文件:
fwrite(data,size,1,fp);
参数释义:
待写入数据、写入大小、默认为1、文件句柄。
读取文件:
fread(data, size, 1, fp);
参数释义:
存储读取数据、读取大小、默认为1、文件句柄
看个实例吧:
#include"public.h"
//打开文件/
FILE * open(char * filename)
{
FILE *fp=NULL;
fp=fopen(filename,"r+");
if (fp==NULL)
{
fp=fopen(filename,"w+");
}
return fp;
}
/用户链表写入文件//
void List_to_file(POINT_T * head,FILE * fp,int size)
{
遍历链表并打印
POINT_T * pNode=head;
rewind(fp);
while(pNode->next!=NULL)
{
pNode=pNode->next;
//
// printf("%s %d\n",pNode->pData->t,pNode->pData->a); 这一行要不要放出来还没想清楚
//将数据写入文件//
fwrite(pNode->pData,size,1,fp);
// printf("\n");
}
fflush(fp);
}
///文件写入链表//
POINT_T * File_to_list( FILE* p, int size)
{
void * data = NULL;
POINT_T * head = NULL;
int ret = -1;
// 创建链表头节点
head = creat(NULL);
rewind(p);
// 对数据域开辟空间
data = malloc(size);
memset(data, 0, sizeof(size));
while (1)
{
// 读取大小为size的数据内容
ret = fread(data, size, 1, p);
if (ret < 1)
{
// 未读取到数据内容,表示文件已到结尾
break;
}
// 添加到链表
add(head, data);
data = malloc(size);
memset(data, 0, sizeof(size));
}
return head;
}
C分文件编程
在写项目的时候,总不可能是把所有代码都写在一个文件里面吧,这样就太low了。
应该根据功能将代码划分到不同的文件中去。
这里有些注意点:
1、创建同名的头文件(.h)和cpp文件。
2、在头文件里写函数声明,在cpp文件中写函数定义。
3、在cpp文件中写#include "xx.h" //自定义头文件名
4、框架(include using namespace std;)写在.h文件中
慢慢就习惯了。
像这样:
接下来,我们盘点一下前边落下的那些知识点:
盘点
运算符
A = 20,B = 10;
运算符 | 描述 | 实例 |
---|---|---|
+ | 把两个操作数相加 | A + B 将得到 30 |
- | 从第一个操作数中减去第二个操作数 | A - B 将得到 10 |
* | 把两个操作数相乘 | A * B 将得到 200 |
/ | 分子除以分母(去尾法保留整数) | B / A 将得到 0 |
% | 取模运算符,整除后的余数 | B % A 将得到 10 |
++ | 自增运算符,整数值增加 1 | A++ 将得到 21 |
– | 自减运算符,整数值减少 1 | A-- 将得到 19 |
关系运算符
运算符 | 描述 | 实例 |
---|---|---|
== | 检查两个操作数的值是否相等,如果相等则条件为真。 | (A == B) 为假。 |
!= | 检查两个操作数的值是否相等,如果不相等则条件为真。 | (A != B) 为真。 |
> | 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 | (A > B) 为真。 |
< | 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 | (A < B) 为假。 |
>= | 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 | (A >= B) 为真。 |
<= | 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 | (A <= B) 为假。 |
逻辑运算符
A = 1,B = 0;
运算符 | 描述 | 实例 |
---|---|---|
&& | 称为逻辑与运算符。如果两个操作数都非零,则条件为真。 | (A && B) 为假 |
两竖杆 | 称为逻辑或运算符。如果两个操作数中有任意一个非零,则条件为真。 | (A 两竖杆 B) 为真 |
! | 称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。 | !(A && B) 为真。 |
赋值运算符
运算符 | 描述 | 实例 |
---|---|---|
= | 简单的赋值运算符,把右边操作数的值赋给左边操作数 | C = A + B 将把 A + B 的值赋给 C |
+= | 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 | C += A 相当于 C = C + A |
-= | 减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数 | C -= A 相当于 C = C - A |
*= | 乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数 | C *= A 相当于 C = C * A |
/= | 除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数 | C /= A 相当于 C = C / A |
%= | 求模且赋值运算符,求两个操作数的模赋值给左边操作数 | C %= A 相当于 C = C % A |
字符串函数
函数 | 目的 |
---|---|
strcpy(s1, s2); | 复制字符串 s2 到字符串 s1。 |
strcat(s1, s2); | 连接字符串 s2 到字符串 s1 的末尾。 |
strlen(s1); | 返回字符串 s1 的长度。 |
strcmp(s1, s2); | 如果 s1 和 s2 是相同的,则返回 0;如果 s1<s2 则返回小于 0;如果 s1>s2 则返回大于 0。 |
strchr(s1, ch); | 返回一个指针,指向字符串 s1 中字符 ch 的第一次出现的位置。 |
strstr(s1, s2); | 返回一个指针,指向字符串 s1 中字符串 s2 的第一次出现的位置。 |
重命名
typedef unsigned char BYTE;
在这个类型定义之后,标识符 BYTE 可作为类型 unsigned char 的缩写
预处理器
在头文件中,一般开头会这么写:
这是一个other.h的文件
#ifndef _OTHER_H_
#define _OTHER_H_
末尾:
#endif
指令 | 描述 |
---|---|
#define | 定义宏 |
#include | 包含一个源代码文件 |
#undef | 取消已定义的宏 |
#ifdef | 如果宏已经定义,则返回真 |
#ifndef | 如果宏没有定义,则返回真 |
#if | 如果给定条件为真,则编译下面代码 |
#else | #if 的替代方案 |
#elif | 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码 |
#endif | 结束一个 #if……#else 条件编译块 |
#error | 当遇到标准错误时,输出错误消息 |
#pragma | 使用标准化方法,向编译器发布特殊的命令到编译器中 |
强制类型转换
强制类型转换是把变量从一种类型转换为另一种数据类型。
像这样:
(type_name) expression
int main()
{
int sum = 17, count = 5;
double mean;
mean = (double) sum / count;
printf("Value of mean : %f\n", mean );
}
强转有风险,使用需谨慎。
static
使用 static 修饰局部变量可以在函数调用之间保持局部变量的值。
static 修饰符也可以应用于全局变量。当 static 修饰全局变量时,会使变量的作用域限制在声明它的文件内。
extern
extern 存储类用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。当您使用 extern 时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置。
盘点完毕,下一篇进项目!!!