C语言基础&&数据结构&&笔记

     上一篇博客学习了C语言的一些基本的入门知识如常见的变量,常量的用法,数据类型的大小判断,转义字符的介绍,以及结构体与结构体指针的运用问题,这次的博客我将继续深入对C语言的学习,再拓展一些数据结构相关的知识。

一、字符串常见的问题

1.1 关于'\0'、0、'0'的区别

'\0':表示字符串结束的标志。

0:表示数字0,在值上等于'\0'。

'0':表示数字字符0,对应的码值为48,和上面两个不是一个概念。

#include <stdio.h>
int main()
{
	char a, c;
	int b;
	a = '\0';
	b = 0;
	c = '0';
	printf("%d\n%d\n%d\n", a, b, c);
	return 0;
}

代码运行结构如上。

二、“~”二进制位取反符的运用

2.1 例子引入

#include <stdio.h>
#include <iostream>
using namespace std;
int main()
{
	int a = 0;
	int b = ~a;
	cout << b << endl;
	return 0;
}

现在给大家一段代码,现在问大家这里输出的b结果是什么?大家会怎么样认为,首先介绍一下“~”这个是按位取反符,因为这里定义的a是一个整型所占大小为32个字节这里也就是“00000000000000000000000000000000”按位取反后变成“11111111111111111111111111111111”,所以可以猜一下b输出的结果是什么。

结果如上,是-1,那么问题来了,这里的结果为什么会等于-1呢?这就与原码、补码、反码有关了。

2.2 原码、反码、补码(后续补充,目前只是概述)

原码、反码、补码的运算关系:原码符号位(这里指“11111111111111111111111111111111”的第一位,表示符号位,符号位为1表示符号,为0表示正号)不变其他位按位取反得到反码,反码再加1得到补码,如果是补码到原码则是以上过程反过来,任何数字在计算机内存中存的都是补码,反码指的是计算时的一个中间状态。

概念计算关系:

原码 --------------------------------------------> 反码 --------------------------------------------> 补码

       (符号位不变,其他位按位取反)                            (加一)

原码 <-------------------------------------------- 反码 <-------------------------------------------- 补码

       (符号位不变,其他位按位取反)                            (减一)

那么回到上述的问题,我们取反得到了补码“11111111111111111111111111111111”,而输出函数printf()输出的是原码,所以要将补码转换成原码首先得让它变成反码,根据上述关系,补码减1变成反码,也就是“11111111111111111111111111111110”,变成反码后再取反就是原码(注意:这个转换过程中的符号位始终不变),取反变成“10000000000000000000000000000001”,这个时候输出的就是上述原码,符号位为1表示符号,所以输出的值为-1。

三、弄巧成拙识命名空间namespace

      因为刚接触了cout与cin两个关键字的使用,就急不可耐的在程序编写中使用,但用到后来运行的时候发生问题:

     那么为什么这里会出现错误呢?在请教一些佬和查阅相关资料后才知道这里的cout,endl(这里指的是换行)无法使用是因为没有使用命名空间std,毕竟这些关键字不会凭空产生,你要用它就得和它们的头头打个招呼,它们的头头是#include <iostream>库里的一个命名空间std,在这个命名空间里cout的作用是输出,可以类比printf()和scanf()函数,它们在使用前必须要有个#include <stdio.h>这个头文件一样。

 3.1 命名空间的背景

     那讲了这么多的命名空间,那命名空间是什么可能大家还不太清楚,下面我就来给大家举个栗子介绍一下吧,首先在C语言的学习过程中我们会定义很多很多的数据名字,这些名字的使用会让我们写代码时更加方便,阅读时更加容易理解,但在取这些名字时不注意就会和库函数的关键字的名字一样。

     还有在以后工作写代码的时候,可能会出现你定义的名字与其他程序员写的代码所定义的名字相同的情况,这样使用起来就会发生问题,为了解决这个问题我们引入命名空间namespa这个概念,使用命名空间后你所定义的函数或变量在使用时就是这个命名空间里的函数或变量,这样在使用函数与变量的过程中就不会与其他人产生冲突了。

 3.2 namespace关键字的使用

具体操作:namespace 局部作用域的名字 {想要作用的区域}

注:这里的namespace是没有生命周期的,所以不论把他放在哪都可以调用,放在主函数里不行,至于为什么,问就是因为C语言的语法上不允许。

#include <stdio.h>
#include <iostream>
namespace A
{
	int printf=10;
}
int main()
{

   printf("%d", A::printf);
	return 0;
}

     上述的程序因为定义的printf整型变量与头文件里的printf()函数名字冲突但我就想要用它来进行输出怎么办,所以我定义了一个命名空间A,里面放了一个名为printf且值为10的整型,这样输出时用命名空间A中的printf就不会担心和printf()函数冲突了。

开始我以为定义一个命名空间A后就可以直接用里面的printf了,但事实往往不能这么想当然,之前我们也提到过,你要想用别人的东西就必须跟它打声招呼,这里使用A中的printf也是一样,使用的格式是:想要访问的命名空间::命名空间里的变量(此处的“::”为作用域限制符)

A::printf

这样就可以调用命名空间A里的东西了。

3.2.1 命名空间结构体的访问

讲了命名空间中比较简单整型变量的使用,肯定会想着更复杂类型在命名空间中要如何去使用,就比如说结构体,我们在命名空间中定义了一个结构体,然后在结构体里定义了一个整型变量  printf,我们应该怎么去调用它呢?下面我用一串代码按照我的理解来给大家解释如何去使用(开始我也并不清楚,后来我请教了身边的学长学姐和老师才弄明白)

#include <stdio.h>
#include <iostream>
namespace A
{
	struct Q
	{
		int printf = 10;
	};
	
}
int main()
{
	struct A::Q a;
   printf("%d", a.printf);
	return 0;
}

这里我们只要定义了这个:“struct A::Q a;”,我们就可以使用结构体Q中的整型变量,这里我按我的想法逐词解释,首先是struct这里表示自定义类型,因为这里的Q是我们自己定义的结构体,然后我们要用命名空间A中的结构体Q,所以是struct A ::Q,但我们学结构体知道,如果要用结构体里的东西就必须定义一个结构体变量,有结构体变量才能去引用结构体里的东西,这里的a就是我定义的一个结构体Q的一个结构体变量,所以是struct A::Q a,然后当我们使用结构体变量a中的东西时直接用a.(结构体里的东西)就行了,这就是为什么输出函数中有a.printf的原因。

3.2.2 命名空间的全局展开

命名空间我们可以把它抽象的比喻成一个保险箱,而保险箱里面的东西要用就得输入密码(这里指的是命名空间的访问),那如果我们不想那么麻烦,想把这个保险箱里的东西全部公开,人人都能用怎么办?这里就用到了命名空间的全局展开:using namespace {命名空间名称}下面用一串代码举例:

#include <stdio.h>
#include <iostream>
namespace A
{
	struct Q
	{
		int b = 10;
	};
	
}
using namespace A;
int main()
{
	struct Q a;
    printf("%d", a.b);
	return 0;
}

这样我们就可以直接访问命名空间里的内容了,展开后就不用输密码什么的直接拿着用就行,当然正常情况下不建议展开使用,可能会发生命名冲突什么的,特殊情况特殊对待。

3.2.3 命名空间的局部展开

当我们的命名空间中有多个东西,全局展开的话可能会发生命名冲突,那么我们能不能只挑我们自己需要的那个展开呢?显然是可以的。具体操作:using 命名空间名称 {想要用的东西},这里我用一个程序来举例:

​​#include <stdio.h>
#include <iostream>
namespace A
{
	struct Q
	{
		int b = 10;
	};
	struct W
	{
		int c = 20;

	};
}
using A::W;
int main()
{
	struct W a;
    printf("%d", a.c);
	return 0;
}

这里我想调用命名空间里的结构体W的里面的一个整型c,用上述的操作只将结构体W展开就可以直接用了。

3.2.4 命名空间的嵌套

上面讲的都是我们定义的名字与头文件的函数发生冲突才定义一个命名空间防止这种问题发生,那当定义多个命名空间时,命名空间的名字发生冲突怎么办?这就要用到命名空间的嵌套了,下面我用代码举个例子:

#include <stdio.h>
#include <iostream>
namespace A
{
	struct Q
	{
		int b = 10;
	};
}
namespace A
{
	namespace B
	{
		struct Q
		{
			int c = 20;

		};
	}
}

int main()
{
	struct A::B::Q a;
    printf("%d", a.c);
	return 0;
}

这里如果不嵌套一个命名空间B的话,系统会把命名冲突的命名空间里的数据整合到一起,那我这里定义的两个结构体的名字就会冲突,那么然后防止冲突,将它放到嵌套命名空间里就行,这样系统整合的时候就不会出现问题。

四、getchar与scanf函数使用时的注意点

说到scanf与getchar,想必大家都不陌生,在学习C语言的第一天起我们就开始接触它们,但我们对它们在输入方面可能有点模棱两可,分不清scanf与getchar真正有什么区别,下面就来分别探讨一下scanf与getchar在输入上的特点。

4.1 scanf()在输入上的特点:

当我们在运用scanf这个函数需要输入字符串或者数字时,scanf并不是直接从我们的键盘当中直接获取输入的信息,而是从缓冲区中获取信息,这个所谓的缓冲区,是我们在键盘上输入后暂时保存输入信息的地方。当对于输入的信息放在缓冲区中存在空格时,scanf函数只能获取空格前的信息(正常情况下),这是scanf函数的弊端。

4.2 getchar()在输入上的特点:

 在输入字符或数字时运用getchar,它只能向缓冲区中读取单个字符,能够读取的内容非常地少,因此我们在运用getchar时大多时候要利用循环来进行读取,但它并不是没有优点,它能够弥补scanf在输入上读取字符的不足。当getchar向缓冲区读取字符时,'\0'与' '它都能够读取,因此我们有时利用循环来判断ch = getchar() != EOF来读取字符

     以上的ch其实是int类型的,总是有不少人会问getchar读取的不是字符吗,为什么会用int去接收呢?仔细思考,getchar只能读取单个字符,那单个字符一定会有它的ASCII码值,那么就要用int类型的变量去接收。

4.3 例子说明:

利用scanf与getchar来输入密码,输入密码后需要确认密码,选择Y则确认成功,否则确认失败。

首先正常思维,错误示范:

 
#include <stdio.h>
int main()
{
	char password[20] = { 0 };
	printf("请输入密码->:\n");
	scanf("%s", password);
	printf("请确认密码:(Y/N):\n");
	int ch = getchar();
	if (ch == 'Y')
	{
		printf("确认成功\n");
	}
	else
	{
		printf("确认失败\n");
	}
		return 0;
}

这里输入密码还没来得及输入Y/N就没了,其实就是缓冲区的问题,当我们输入abcdef再输入一个回车时,scanf就在缓冲区内读取了abcdef,而回车也代表一个字符'\n',getchar就从缓冲区上读取了这个回车,回车不等于Y,则确认失败。正确的代码应该为:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
	char password[20] = { 0 };
	printf("请输入密码->:\n");
	scanf("%s", password);
	getchar();
	printf("请确认密码:(Y/N):\n");
	int ch = getchar();
	if (ch == 'Y')
	{
		printf("确认成功\n");
	}
	else
	{
		printf("确认失败\n");
	}
	return 0;
}

我们修改的地方只是在scanf的后面加了一个getchar读取走输入abcdef后的回车,此时缓冲区内没有任何字符,这时我们就能在确认密码时输入Y或N,但它仍有一定的弊端,在我们输入ab cd时问题又会出现,运行时又会在没有输入Y或N就确认失败,结果如图:

出现bug的原因在于scanf只会在缓冲区内接收ab,空格和cd它并没有接受,当只有一个getchar时只能读取空格,这时我们应该利用循环,多次用getchar来读取字符,直到读取的是\0为止。

#include <stdio.h>
int main()
{
	char password[20] = { 0 };
	printf("请输入密码->:\n");
	scanf("%s", password);
	getchar();
	while (getchar() != '\n')
	{
		;
	}
	printf("请确认密码:(Y/N):\n");
	int ch = getchar();
	if (ch == 'Y')
	{
		printf("确认成功\n");
	}
	else
	{
		printf("确认失败\n");
	}
		return 0;
}

杂项笔记:

这里记的内容都是编程中的一些细节拓展问题,写的时候不明白查资料理解后就简略的记下来了。

1. size_t类型的说明:

size_t表示无符号类型整型,正常的int类型表示的是有符号类型整型,这里的符号指的是正负号,无符号的意思相当于只能去是大于0的值,它是为了方便系统之间的移植而定义的,不同的系统上,定义size_t 可能不一样。size_t在32位系统上定义为 unsigned int,也就是32位无符号整型。在64位系统上定义为 unsigned long ,也就是64位无符号整形。size_t 的目的是提供一种可移植的方法来声明与系统中可寻址的内存区域一致的长度。

size_t 在数组下标和内存管理函数之类的地方广泛使用。例如,size_t 用做sizeof 操作符的返回值类型,同时也是很多函数的参数类型,包括malloc 和strlen。

在声明诸如字符数或者数组索引这样的长度变量时用size_t 是好的做法。它经常用于循环计数器、数组索引,有时候还用在指针算术运算上。size_t 的声明是实现相关的。它出现在一个或多个标准头文件中,比如stdio.h 和stblib.h,典型的定义如下:

#ifndef __SIZE_T
#define __SIZE_T
typedef unsigned int size_t;
#endif

define 指令确保它只被定义一次。实际的长度取决于实现。通常在32 位系统上它的长度是32 位,而在64 位系统上则是64 位。一般来说,size_t 可能的最大值是SIZE_MAX。

打印size_t 类型的值时要小心。这是无符号值,如果选错格式说明符,可能会得到不可靠的结果。推荐的格式说明符是%zu。不过,某些情况下不能用这个说明符, 作为替代,可以考虑%u 或%lu

下面这个例子将一个变量定义为size_t,然后用两种不同的格式说明符来打印:

size_t sizet = -5;
printf("%d\n",sizet);
printf("%zu\n",sizet);

因为size_t 本来是用于表示正整数的,如果用来表示负数就会出问题。如果为其赋一个负数,然后用%d 和%zu 格式说明符打印,就得到如下结果:

-5
4294967291

%d 把size_t 当做有符号整数,它打印出-5 因为变量中存放的就是-5。%zu 把size_t 当做无符号整数。当-5 被解析为有符号数时,高位置为1,表示这个数是负数。当它被解析为无符号数时,高位的1 被当做2 的乘幂。所以在用%zu 格式说明符时才会看到那个大整数。

sizet = 5;
printf("%d\n",sizet); // 显示5
printf("%zu\n",sizet); // 显示5

因为size_t 是无符号的,一定要给这种类型的变量赋正数 。

1.1size_t与int的区别
  • size_t在32位架构中定义为:typedef   unsigned int size_t;
  • size_t在64位架构中被定义为:typedef  unsigned long size_t;
  • size_t是无符号的,并且是平台无关的,表示0-MAXINT的范围;int为是有符号的;
  • int在不同架构上都是4字节,size_t在32位和64位架构上分别是4字节和8字节,在不同架构上进行编译时需要注意这个问题。

2.assert()函数:

断言函数,顾名思义就是中的程序的进行,该函数使用时会判断()里的值是否为假,若为假,则中断程序的进行直接结束,可以和if()语句进行对比,if语句是判断()里是否为真,为真则执行,为假则跳过,而assert函数则想法,为假执行,为真跳过。

下面举个例子解释:

#include <stdio.h>
#include <assert.h>
int main()
{
    int m, n, result;
    scanf("%d %d", &m, &n);
    assert(n != 0);  //写作 assert(n) 更加简洁
    result = m / n;
    printf("result = %d\n", result);
    return 0;
}

本例用来计算两个数相除的结果,由于被除数不能为 0,所以我们加入了 assert() 来检测错误。如果输入100 20,那么 n 的值为 20,n != 0这个条件成立,assert() 不进行任何操作,最终的输出结果为:

    如果输入100 0,那么 n 的值为 0,n != 0这个条件不成立,assert() 就会报告错误,并终止程序执行,最终的结果如下所示:

本次的博客内容到此结束,下篇博客会继续深入C语言等相关知识。

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值