指针练习题1

判断打印结果:

#include <stdio.h>

int main(int argc, char* argv[])
{
	char* c[] = { "ENTER", "NEW", "POINT", "FIRST" };
	char** cp[] = { c + 3, c + 2, c + 1, c };
	char*** cpp = cp;

	printf("%s\n", **++cpp);
	printf("%s\n", *-- * ++cpp + 3);
	printf("%s\n", *cpp[-2] + 3);
	printf("%s\n", cpp[-1][-1] + 1);

	return 0;
}

结果:

WindowsAPI 读取Windows注册表信息

读取Windows注册表信息一般会用到3个函数。

1.RegOpenKeyExW()

LSTATUS RegOpenKeyExW(
  HKEY    hKey,      //主键或打开注册表项的句柄
  LPCWSTR lpSubKey,  //要打开的注册表子键的名称 如:L"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0"
  DWORD   ulOptions, //一般为0
  REGSAM  samDesired,//打开的键的所需访问权限
  PHKEY   phkResult  //接收已打开键的句柄的变量的指针
);
如果函数成功,则返回值为ERROR_SUCCESS。

如果函数失败,则返回值是Winerror.h中定义的非零错误代码。可以将FormatMessage函数与FORMAT_MESSAGE_FROM_SYSTEM标志一起使用, 以获取错误的一般描述。

 

2.RegQueryValueExW()

LSTATUS RegQueryValueExW(
  HKEY    hKey,       //主键或打开注册表项的句柄
  LPCWSTR lpValueName,//键值的名称
  LPDWORD lpReserved, //此参数是保留的,必须为NULL
  LPDWORD lpType,     //指向键值相同数据类型的指针
  LPBYTE  lpData,     //指向接收键值数据的缓冲区的指针(注意需强制类型转换为LPBYTE)
  LPDWORD lpcbData    //指向变量的指针,该变量指定lpData参数指向的缓冲区大小。函数返回时,此变量包含复制到lpData的数据的大小。
);
如果函数成功,则返回值为ERROR_SUCCESS。

如果函数失败,则返回值是系统错误代码。

如果lpData缓冲区太小而无法接收数据,则该函数返回ERROR_MORE_DATA。

如果lpValueName注册表值不存在,则该函数返回ERROR_FILE_NOT_FOUND。

 

3.RegCloseKey()

LSTATUS RegCloseKey(
  HKEY hKey         //要关闭的打开键的句柄
);
如果函数成功,则返回值为ERROR_SUCCESS

 

具体示例如下:

	BOOL			result;
	HKEY			hkey;
	LSTATUS			reg_return;
	DWORD			data_type_dword;
	DWORD			data_size;

	data_size = sizeof(MaidInfo.cpu_frequency);

	reg_return = RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", 0, KEY_READ | KEY_WOW64_64KEY, &hkey);
	if (reg_return != ERROR_SUCCESS)
	{
		printf("cpu_frequency open faild. return is %d\n", reg_return);
	}

	reg_return = RegQueryValueExW(hkey, L"~MHz", NULL, &data_type_dword, (LPBYTE)& MaidInfo.cpu_frequency, &data_size);
	if (reg_return != ERROR_SUCCESS)
	{
		printf("cpu_frequency query value faild. return is %d\n", reg_return);
	}

	printf("cpu_frequency: %u\n", MaidInfo.cpu_frequency);
	printf("data_size: %u\n\n", data_size);

	RegCloseKey(hkey);

 

回调函数1(Callback Function)

#include <stdio.h>

//回调函数:
//回调函数就是一个通过函数指针调用的函数。
//如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。

//函数指针:
//void (*p)(void);		指向一个参数和返回值均为空的函数指针
//void (*p)(int, int);	指向有两个整型参数,返回值为空的函数指针
//char (*p)(int);		指向参数为整型,返回值为字符型的函数指针
//void *(*p)(int*);		指向void *fan(int *a)这样的函数

//函数指针的赋值:
//p = fan;	让指针p指向fun整个函数

void fun1(void)
{
	printf("It's function 1.\n");
}

void fun2(void)
{
	printf("It's function 2.\n");
}

int main(int argc, char* argv[])
{
	void (*p)(void);
	p = fun1;			//让指针p指向fun整个函数

	(*p)();				//运行函数指针
	p();				//运行函数指针的简化写法

	return 0;
}

//1.声明函数
//2.声明相同类型的函数指针
//3.将函数的值赋给函数指针
//4.运行函数指针

注:

在软件分层时经常用到回调函数。

上传文件函数

BOOL File_Upload(char *path, char *sendbuffer, int bufferlen, SOCKET sock)
{
	HANDLE	fHandle;
	BOOL	Result;
	BOOL	DataSend;
	DWORD	FileSize = 0;
	DWORD	dwBytesRead = 0;
	DWORD	oncelen = 0;
	DWORD	totollen = 0;

	fHandle = CreateFileA(path, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (fHandle == INVALID_HANDLE_VALUE)
	{
		printf("Upload GreateFile Faild.\nError Code is %d\n", GetLastError());
		return FALSE;
	}
	FileSize = GetFileSize(fHandle, NULL);
	sprintf_s(sendbuffer, bufferlen, "%d", FileSize);
	sendbuffer[strlen(sendbuffer)] = 0;
	DataSend = SendPacket(sock, sendbuffer, strlen(sendbuffer) + 1);
	if (DataSend == FALSE)
	{
		printf("Upload FileSize Send Faild.\n");
		return FALSE;
	}
	do
	{
		if (FileSize - totollen > bufferlen)
		{
			oncelen = bufferlen;
		}
		else
		{
			oncelen = FileSize - totollen;
		}
		do
		{
			Result = ReadFile(fHandle, sendbuffer, oncelen, &dwBytesRead, NULL);
			if (Result == 0)
			{
				printf("Upload ReadFile Faild : %u", GetLastError());
				return FALSE;
			}
		} while (dwBytesRead < oncelen);
		DataSend = SendPacket(sock, sendbuffer, dwBytesRead);
		if (DataSend == FALSE)
		{
			printf("FileDownload Send Faild.\n");
			return FALSE;
		}
		totollen += oncelen;
	} while (totollen < FileSize);
	CloseHandle(fHandle);
	return TRUE;
}

注:

1.函数的buffer长度需要通过一个参数传入。

2.使用ReadFile函数时,如果文件句柄未关闭,可以接着上次读到的结尾继续读。

比较字符串是否一致

在比较两个字符串是否一致时,不能用字符串名称直接比较。因为那样比较的是字符串地址。

正确的方法应使用strcmp()函数

例:

while (node != &head)
{
	SockInfo = CONTAINING_RECORD(node, struct SocketInfo, Entry);
	if (strcmp(SockInfo->UserID, dstID) == 0)
	{
		SendDataLen = strlen(RecvBuffer) + 1;
		Forword = SendPacket(SockInfo, RecvBuffer, SendDataLen);
		SendDataLen = strlen(UserID) + 1;
		IDSend = SendPacket(SockInfo, UserID, SendDataLen);
		break;
	}
	node = node->next;
}

strcmp()函数中的两个字符串等于0,则表示两个字符串相等。

 

另外需要特别注意,类似以上例子中在循环中寻找单一目标时要及时跳出,减少计算资源的浪费。

send函数和recv函数

一、send函数和recv函数的基本用法

send和recv是C语言中的函数,分别用于向一个已连接的socket发送和接收数据。

1.send()

	int send(
		SOCKET     s,		//一个用于标识已连接套接口的描述字
		const char *buf,	//指向包含要传输数据的缓冲区的指针
		int        len,		//buf参数指向缓冲区中的数据的长度,以字节为单位
		int        flags	//指定调用的方式,该参数一般填0
	);

返回值:

返回值 > 0,send返回发送的字节总数,它可能小于len参数中请求发送的字节数;

返回值 = 0,连接已优雅的关闭;

返回值 < 0,发生错误,返回值为SOCKET_ERROR值,可以通过调用WSAGetLastError检索特定的错误代码。

https://docs.microsoft.com/zh-cn/windows/desktop/api/winsock2/nf-winsock2-send

2.recv()

	int recv(
		SOCKET s,			//一个用于标识已连接套接口的描述字
		char   *buf,		//指向接收传入数据的缓冲区的指针
		int    len,			//buf参数指向的缓冲区的长度(以字节为单位)
		int    flags		//指定调用的方式,该参数一般填0
	);

返回值:

返回值 > 0,recv返回接收到的字节数;

返回值 = 0,连接已优雅的关闭;

返回值 < 0,发生错误,返回值为SOCKET_ERROR值,可以通过调用WSAGetLastError检索特定的错误代码。

https://docs.microsoft.com/zh-cn/windows/desktop/api/winsock/nf-winsock-recv

 

二、send函数和recv函数的包装

1.包装send函数

由于send函数返回的字节总数可能小于len参数中请求发送的字节数,也无法预计一次传输会发送多少数据。所以要对send函数进行包装后再使用。包装思路如下:

1)确保实际发送的字节数等于要发送的字节数。即必须在发送缓冲区中收集到要发送的全部数据才返回成功。

2)对数据进行切块,每次至多发送32KB。即小于32KB的数据一次发送,大于32KB的数据每次发送32KB,循环n个32KB后发送剩余数据。

结果如下:

	BOOL Send(SOCKET sock, char* buffer, int length)
	{
		int once;			//一次发送的返回值
		int total = 0;		//目前发送了多少数据
		int thislen = 0;	//本次要发送的数据长度

		do
		{
			if (length - total > 32 * 1024)		//判断剩余数据是否大于32KB,如果大于32KB
				thislen = 32 * 1024;			//则本次发送32KB,完成数据切块
			else
				thislen = length - total;		//如果小于等于32KB,则一次将剩余数据发完
			once = send(sock, buffer + total, thislen, 0);
			if (once <= 0)						//如果once < 0则表示发送失败,用break跳出循环,返回FALSE
			{
				break;
			}
			total += once;
		} while (total < length);				//如果total < length说明数据还未完全发送,继续循环

		if (total == length)					//当total == length时,说明已完全发送,返回TRUE
			return TRUE;
		return FALSE;
	}

2.包装recv函数

首先,recv函数与send函数有相似的问题,即一次接受未必能够收全数据。需要循环接受数据,再进行判断,看已接收到的数据是否等同于需要接受的数据。其次,recv函数还存在一个send函数没有的问题。

以由用户在客户端输入字符串再由客户端向服务端发送数据为例。在客户端可以使用如strlen函数计算需要发送的数据长度,在send时判断是否发送完全。而单次的收发数据中,接收端使用recv函数时无法提前得知接收多少数据才是完整的,所以无法保证接收到了完整的数据。

要解决这个问题,可以采用分两次发送接收的方式。发送端先发送一个数据包头,包头中携带的信息为接下来实际数据的长度。这样在接下来调用recv函数时,就可以在len参数中填入实际的数据长度了。而包头的长度可以事先约定好,如长度固定为4字节。

结果如下:

	BOOL Recv(SOCKET sock, char* buffer, int length)
	{
		int once = 0;
		int total = 0;
		BOOL r = FALSE;

		do
		{
			once = recv(sock, buffer + total, length - total, 0);
			if (once <= 0)
			{
				printf("error.");
				break;
			}
			total += once;
		} while (total < length);
		if (total == length)
		{
			r = TRUE;
		}
		return r;
	}

发送数据包头时将length参数设为4字节即可。

 

三、包装一次完整的发送和接收

通过上面包装好的函数,在一次完整的发送接收过程中,需要调用两次Send函数和两次Recv函数。在确定数据包头和实际数据的关系后,可以对该函数进一步包装,以达到使代码逻辑更清晰的效果。

以下为进一步包装的发送接收函数:

1.发送函数

BOOL Send(SOCKET sock, char* buffer, int length)
{
	int once;
	int total = 0;
	int thislen = 0;

	do
	{
		if (length - total > 32 * 1024)
			thislen = 32 * 1024;
		else
			thislen = length - total;
		once = send(sock, buffer + total, thislen, 0);
		if (once <= 0)
		{
			break;
		}
		total += once;
	} while (total < length);

	if (total == length)
		return TRUE;
	return FALSE;
}

BOOL SendPackage(SOCKET sock, char* buffer, int length)
{
	if (Send(sock, (char*)&length, sizeof(length)) && Send(sock, buffer, length))
	{
		return TRUE;
	}
	return FALSE;
}
/*该函数中用到了强制类型转换,以及逻辑与运算。
程序会先完整执行&&之前的表达式,如果结果为假,
则不会执行后面的表达式,直接返回FALSE。*/

2.接收函数

BOOL Recv(SOCKET sock, char* buffer, int length)
{
	int once = 0;
	int total = 0;
	BOOL r = FALSE;

	do
	{
		once = recv(sock, buffer + total, length - total, 0);
		if (once <= 0)
		{
			printf("error.");
			break;
		}
		total += once;
	} while (total < length);
	if (total == length)
	{
		r = TRUE;
	}
	return r;
}

BOOL RecvPackage(SOCKET sock, char* buffer, int *length)
{
	int buflen = *length;		//注意*length既是输入参数又是输出参数,一般可以输入buffer的长度
	int packet_length;
	BOOL r = FALSE;

	*length = 0;
	if (Recv(sock, (char*)&packet_length, sizeof(packet_length)))	//先发一个4字节的数据包头
	{
		if (packet_length <= buflen)					//判断发送的数据长度是否大于缓冲区长度
		{
			if (Recv(sock, buffer, packet_length))		//接收实际数据
			{
				*length = packet_length;				//将实际的数据长度赋值给*length
				r = TRUE;
			}
		}
	}
	return r;
}

 

do-while循环的活用

do-while循环是 while 循环的变体。

在检查while()条件是否为真之前,该循环首先会执行一次do{}之内的语句。

然后在while()内检查条件是否为真,如果条件为真的话,就会重复do-while这个循环,直至while()为假。

while循环则是先判断条件是否为真,再进行循环。

 

根据do-while循环的特点,可以对其进行活用,以减少在包含大量if判断语句的程序中大量使用return的问题。

同时还可以避免goto的使用。

 

以套接字socket函数为例。

#include <stdio.h>													
#include <WinSock2.h>					//调用socket函数需要该头文件

int main(int argc, char* argv[])
{
	SOCKET ServerSocket = 0;			//最好在SOCKET类型变量声明时赋初值,便于释放资源时进行判断

	do
	{
		ServerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	    /*ServerSocket为socket函数的返回值,该函数包含3个参数,分别为(Internet地址描述,socket类型TCP或UDP,协议类型)*/

		if (INVALID_SOCKET == ServerSocket)		/*若无错误发生,socket()返回引用新套接口的描述字。否则的话,返回INVALID_SOCKET错误。
												此处如果满足条件socket申请失败,则跳出循环*/
		{                                       
			printf("Socket invoke failed !\n");
			break;
		}

		while (1)			//常用的死循环表达方式
		{
			/* ...... */
		}
	} while (0);		    /*do-while循环若while判断条件为0,则该循环只有第一次能够执行。 
	                          将整段函数置于do-while中,break的使用,可以在满足判断条件的情况下直接跳到代码尾部。*/

	if (ServerSocket)		//即(ServerSocket != 0),也就是在申请到socket的情况下,使用closesocket关闭后再退出,以达到施放资源的效果。
	{
		closesocket(ServerSocket);
	}
	return 0;

在使用一个不熟悉的函数时,需要确认以下几点:

1.该函数是否有返回值。如有返回值,其类型是什么,代表什么。

2.使用该函数需要用到几个参数,参数的数据类型及含义。

 

Windows API可以在微软官网https://docs.microsoft.com上查到详细解释。

 

字符串的基本知识

一、字符串的字面量

像”ABC”这样带双引号的一系列字符称为字符串字面量

在字符串字面量的末尾会被加上一个叫做null字符的值为0的字符。用八进制转义字符表示null字符就是’\0’。

null字符的每一位都是0。若不用字符常量,而用整数常量表示就是0。

如果要在字符串字面量中表示双引号”,就需要使用转义字符\”。例如要表示 XY”Z的字符串字面量就必须写作”XY”Z”。

 

二、字符串

字符串的字面量类似于整数的50、浮点数的3.14等常量。数值型数据可以通过变量(对象)的数据类型转换进行混合运算。而表示字符序列的字符串(string)也可以以对象形式保存并灵活处理。

C语言中的字符串是以字符序列,即字符数组实现的。例如要表示字符串”ABC”,数组元素必须按以下顺序依次保存:

“A”、”B”、”C”、”\0”

末尾的null字符”\0″是字符串结束的标志。

 

三、字符数组的初始化赋值

有三种赋值方式:

	char str[5];

	str[0] = 'A';
	str[1] = 'B';
	str[2] = 'C';
	str[3] = 'D';
	str[4] = '\0';

 

	char str[5] = {'A','B','C','D','\0'};

 

	char str[5] = "ABCD";

 

在声明中,元素数可以省略。此时,数组str的元素数视为5。

	char str[] = "ABCD";

 

按位运算符与逻辑运算符

一、按位运算符

按位与运算符 & a & b 按位计算a和b的逻辑与
按位或运算符 | a | b 按位计算a和b的逻辑或
按位异或运算符 ~ a ~ b 按位计算a和b的逻辑异或(相同为0,不同为1)

 

二、逻辑运算符

逻辑与运算符 && a && b 对操作数的值进行逻辑与运算
逻辑或运算符 || a || b 对操作数的值进行逻辑或运算

整型常量

十进制常量

日常生活中使用的10、57等整型常量成为十进制常量。

 

八进制常量

以0开头,以区别十进制常量。

12 —— 十进制常量(十进制的12)

012 —— 八进制常量(十进制的10)

 

十六进制常量

以0x或者0X开头。A~F不区分大小写。

0xA —— 十六进制常量(十进制的10)

0x12 —— 十六进制常量(十进制的18)