怎么做网上网站的网站,WordPress 主题解密,游戏开发设计公司,张家界有没有做网站的公司文章目录 前言一、C关键字二、命名空间2.1引子2.2命名空间定义2.3命名空间的使用 三、C输入和输出3.1输出3.2输入 四、缺省参数4.1全缺省4.2半缺省 五、函数重载5.1重载概念 六、引用6.1定义6.2引用的使用示例6.2.1引用作参数6.2.1引用作返回值 6.3传值、传引用效率比较6.4常引… 文章目录 前言一、C关键字二、命名空间2.1引子2.2命名空间定义2.3命名空间的使用 三、C输入和输出3.1输出3.2输入 四、缺省参数4.1全缺省4.2半缺省 五、函数重载5.1重载概念 六、引用6.1定义6.2引用的使用示例6.2.1引用作参数6.2.1引用作返回值 6.3传值、传引用效率比较6.4常引用6.5引用和指针的区别 七、内联函数八、auto关键字九、基于范围的for循环9.1范围for的语法9.2 范围for的使用条件 十、指针空值nullptr 前言
本篇C入门主要是讲C对C语言不足的一些弥补把这部分学好为我们以后C进阶打好坚实的基础。
一、C关键字
C总计有63个关键字其中C语言的就占了32个。
所谓关键字就是我们用来控制语法的关键标识符比如类型、循环、判断等等
二、命名空间
2.1引子
我们先来看一个C里面的常见问题 现在我想打印rand这个变量的值
#includestdio.h
int rand 0;
int main()
{printf(%d,rand);return 0;
}可以看出这里的代码是没有问题的也正常打印了0这个值
但如果我们再加一个头文件#includestdlib.h
#includestdio.h
#includestdlib.h
int rand 0;
int main()
{printf(%d,rand);return 0;
}发现这里居然报错了 这里的报错原因是因为我们的stdlib这个头文件里面也有rand命名的函数与我们这里命名的全局变量rand产生了冲突。
这就是C语言的一大缺陷在我们的项目中难免会有相同名字的变量、函数等但是一旦命名相同C语言就无法编译通过
所以C旨在改进这一缺陷引入了“命名空间”这一概念 具体做法就是在指定的名称前加一个namespace有点类似我们C语言学习的结构体
2.2命名空间定义
namespace test{//这里的命名空间叫test你也可以取其他名字int rand 0;
}//与结构体不一样的是C的命名空间不用加分号int main()
{printf(%d,rand);return 0;
} 那命名空间到底是什么东西 我们大白话说就是现在我家住在山上我家里养了几头猪然后山里也有一些野猪 那我们为了区别到底是野猪还是家猪那我们就装一个围栏把我们的家猪给围起来。
这个围栏就是namespace
但是细心的同学会发现这里还是有一个警告这个警告其实就是告诉我们%d需要的是int型但我们给的是指针类型如果需要打印指针类型就是%p
如果就是要打印rand的值那我们就需要用到“围栏的钥匙”也就是域作用限定符
int main()
{printf(%p\n, rand);//%p打印的是地址printf(%d, test::rand);//如果要找到家猪rand需要找到对应家猪的围栏test//这里的::就是域作用限定符相当于围栏的钥匙return 0;
}所以有了命名空间我们就可以解决命名冲突的问题如果不同程序员定义了多个同名变量那就设多个命名空间通过域作用限定符来使用不同的同名变量
int main()
{printf(%d\n, test::rand);//如果要找到家猪需要找到对应家猪的围栏//这里的::就是域作用限定符printf(%d\n, test1::rand);printf(%d\n, test2::rand);return 0;
}2.3命名空间的使用
命名空间除了定义变量也可以定义函数、结构体等
namespace test{//命名空间testint rand 0;//可以定义变量int add(int x, int y)//可以定义函数{return x y;}struct LNode {//可以定义结构体struct LNode* next;int val;};
}命名空间就是建了一个围栏你要找猪默认不会去围栏里去找。如果想要找围栏里的猪请给出“围栏名称和围栏钥匙”也就是命名空间的名字和作用域限定符::
比如这里我想调用add函数但是没有给命名空间和作用域限定符编译器就会报错
我们把命名空间加上代码就可以正确运行了
namespace test{//命名空间testint rand 0;//可以定义变量int add(int x, int y)//可以定义函数{return x y;}struct LNode {//可以定义结构体struct LNode* next;int val;};
}int main()
{int xtest::add(1, 2);printf(%d, x);return 0;
}命名空间的结构体也是类似的比如 struct test::LNode node; 相当于是struct LNode node 只不过这里中间的LNode是定义在test里所以我们把LNode换成test::LNode
namespace test{//命名空间testint rand 0;//可以定义变量int add(int x, int y)//可以定义函数{return x y;}struct LNode {//可以定义结构体struct LNode* next;int val;};
}int main()
{struct test::LNode node;//相当于是struct LNode node//只不过这里中间的LNode是定义在test里所以我们把LNode换成test::LNodereturn 0;
}可能会有老铁问“那如果我命名空间里的变量同名产生冲突怎么办”
答那你就在命名空间里再定义一个命名空间呗每有一层命名空间就多用一个作用域限定符::
namespace test{//命名空间testint rand 0;//可以定义变量namespace test1 {int rand 1;}
}int main()
{int x test::rand;int y test::test1::rand;printf(x%d\n, x);printf(y%d\n, y);return 0;
}但是这样做的话还有一个问题你每次想要用自己定义的在命名空间那个变量/函数/结构体都要加上命名空间和作用域限定符::这你不觉得烦吗
所以我们这里可以用
using namespace 命名空间名称;这样就默认了到某个命名空间里去用它的变量/函数/结构体
使用了这种你可以直接访问命名空间的也可以用来访问
#includestdio.hnamespace test{//命名空间testint rand 0;//可以定义变量
}using namespace test;
int main()
{int x test::rand;int y rand;printf(x%d\n, x);printf(y%d\n, y);return 0;
}这就有了我们经常用的
using namespace std;std是我们c官方定义的命名空间
ps:在工程项目中不要轻易展开std库因为很容易你自己写的和库中的冲突。日常自己练习一般无所谓。
三、C输入和输出
3.1输出
到这里大家可能会发现我们前面好像打印输出函数还是我们c语言的那一套那我们c有没有自己的呢有
#includeiostream
using namespace std;
int main()
{cout hello world ;//“”是流插入运算符,cout是std库里的一个//c是console控制台的意思out是出来的意思//这里涉及到c的对象知识我们先学怎么用具体的原理后面学习面向对象我会详细介绍//你可以理解为hello world流向了cout控制台//cout很牛逼在于它可以自动识别类型int a 1;float b 3.14;char c x;cout a\n;//如果要换行就在后面跟一个\n表示\n也流向了cout控制台cout bendl;//换行法二在后面加一个endl表示换行法二是最常见的法一有点麻烦了//endl表示end 这个line就是结束这一行的意思cout cendl;//你也可以一次性全部流入比如cout a b c;return 0;
}ps我们直接using namespace std其实是风险很大的因为你一个人直接展开库里面的全部就会导致别人很可能定义的变量和命名空间里的产生冲突所以我们这里建议只展开std里面的cout和endl
using std::cout;
using std::endl;另外还有一点如果你想控制打印出来的精度比如我想打印一个浮点数只到小数点后1位c是不太方便的这里你还是用c语言的printf来控制c语言能用的c也可以用
int main()
{float x 3.1415926;printf(%.2f, x);
}3.2输入
这里的输入也就对应了C语言的scanf 我们c里面就是cin c是console控制台的意思in是进去的意思
需要注意的是因为我们这里是输入嘛所以用cin这里的符号和输出也是反过来的
using std::cout;
using std::endl;
using std::cin;
int main()
{int a 0;cout 请输入a值:;cin a;cout a值为a;//cout叫流插入//cin叫流提取它同样可以自动识别类型
}四、缺省参数
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时如果没有指定实参则采用该形参的缺省值否则使用指定的实参
4.1全缺省
所谓全缺省就是你定义的函数这个函数的参数你都预先赋了值
using namespace std;
void func(int a 1,int b2, int c3) {cout a endl;cout b endl;cout c endl;
}
int main() {func();//缺省a和b和c默认a1b2,c3func(10);//缺省b和c默认b2,c3func(10,20);//缺省c默认c3func(10,20,30);
}4.2半缺省
这种缺省方式就是有一部分参数预先赋了值
注意
半缺省参数必须从右往左依次来给出不能间隔着给缺省参数不能在函数声明和定义中同时出现缺省值必须是常量或者全局变量C语言不支持编译器不支持 如果采用了半缺省你就不能一个参不传了。 因为半缺省肯定是有参数没有提前定义的如果你一个参数不传那那个没有提前定义的参数就没有值给它了就会报错。
using namespace std;
void func(int a ,int b2, int c3) {cout a endl;cout b endl;cout c endl;
}
int main() {func(10);//10传参给a缺省b和c默认b2c3func(10,20);//10和20传参给a和b缺省c默认c3func(10,20,30);//1020,30传参给abc
}五、函数重载
函数重载有点像我们中文的“一词多义”
5.1重载概念
说白了就是函数名相同参数类型不一样、个数不一样、参数顺序不一样返回值可以不一样,可以一样
psC语言是不支持同名函数的但是我们c可以支持只是有一些限制
using namespace std;
int add(int a, int b) {cout int add(int a,int b) endl;return a b;
}double add(double a, double b) {cout double add(double a, double b) endl;return a b;;
}int main()
{int xadd(1, 2);cout x endl;double yadd(1.1, 2.2);cout y endl;return 0;
}小细节如果你有两个不同的重载这时你传的参数和这两个重载都不一样比如 这里就会有歧义了因为有两个重载你到底是把int类型的1转成double还是把double类型的2.2转成int类型呢所以这里会报错
但是如果你只有一个函数那你这样传就没有毛病了编译器会自动给你进行类型转换 因为现在只有一个函数了只要把传参过去类型不对的转换一下就行没有重载函数的歧义
六、引用
6.1定义
这是对C语言改进最大的地方C里面就没有指针的概念了当年C的祖师爷也觉得指针过于复杂
引用不是新定义一个变量而是给已存在变量取了一个别名编译器不会为引用变量开辟内存空 间它和它引用的变量共用同一块内存空间。
using namespace std;
int main()
{int a 1;int b a;int c b;cout a endl;cout b endl;cout c endl;cout a endl;cout b endl;cout c endl;return 0;
}可以看到abc共享一个空间值也是一样的
比如现在有a、b、c三个变量 b是把a的值赋过去b和a不是一个变量它们独占不同空间
但是我们这里int ca这里就不一样了c其实就是a的别名 c和a其实是一个东西它们占用同一份空间
如果你这里b那么a不会受到任何影响 但如果你对c–不好意思a就是cc就是a即a– ps引用必须初始化如果你int b不给b初始化谁知道这个b是哪个变量的别名所以c的引用必须初始化。
c的别名一旦确定中途是不能改变对象的比如a起了别名b那b就一直是a的别名
int main()
{int a 0;int b a;int c 1;b c;//这里是把c值赋给b还是让b称为c的别名cout b endl;//打印1cout a endl;//a和b同地址a和c不同地址cout b endl;cout c endl;//说明bc是赋值,是把c的值赋给bb是a的别名也就是把c的值赋给a//但是b还是a的引用
}6.2引用的使用示例
6.2.1引用作参数
我们c语言经常写一个交换函数swap
int swap(int* x,int* y){int tmp*x;*x*y;*ytmp;
}那我们c就不需要这么麻烦了我们之前说过引用就是取别名引用就是它自己它自己就是引用。
所以我们c这里swap函数就很容易写了
void swap(int x, int y) {int tmp x;x y;y tmp;
}
int main()
{int x 1;int y 2;swap(x, y);coutx x endl;couty y endl;return 0;
}6.2.1引用作返回值
如果不是引用作为返回值比如下面代码 我们很容易知道x的是1
int count()
{int n 0;n;return n;
}
int main()
{int x count();cout x endl;return 0;
}这段代码我们先是在main函数里创立了ret变量然后调用Count函数 创立Count函数栈帧Count里又会创建变量n 这里Count函数调用完栈帧就销毁了所以我们返回的其实是n的复制返回值为1。 因为你栈帧已经销毁了n也肯定销毁了你再返回n的位置不就是野指针吗 但如果我们用的是引用返回 我们知道引用是我们某个变量的别名引用就是那个变量 那你返回引用就是返回那个变量 可以看到如果我们用引用做返回值这里是报了一个警告的 因为你count函数结束其实栈帧已经销毁了 如果你还要访问原先函数里的变量其实是非法访问。
至于这里返回值和前面的一样都是1只是因为vs这个编译器是1如果换别的编译器结果就不一定 如果用引用做返回值返回值是不确定的 因为你用引用做返回值返回值到底是多少是取决于函数栈帧销毁后到底有没有把原先的空间清理掉如果清理掉那就不一定是原来的值了。我们这里vs编译器函数栈帧销毁后是没有清理原先空间至于其他编译器就未可知了。
比如下面这个示例大家就会对我上面说的话有更深的理解
int count()
{int n 0;n;return n;
}
int main()
{int x count();cout x endl;cout x endl;count();cout x endl;return 0;
}我们这里用引用x来接收count函数返回的引用这就意味着x和n是一个位置的 第一次我们打印了1那是因为count函数栈帧里的东西还没销毁 第二次打印了一个1462811616这个数就是count栈帧里东西销毁了这个数可能是其他程序当时正在用那个位置生成的数。
第三次由于我们又调用了count导致n那个位置又生成了1所以我们第三次打印出来是1
再举一个例子
int add(int a,int b)
{int c a b;return c;
}
int main()
{int x add(1,2);cout x endl;add(3,4);cout x endl;return 0;
}这里我们用x来接收add函数的引用 第一次我们xadd1,2并且由于编译器原因函数结束栈帧销毁原先变量位置并没有清除所以打印了3
但第二次我们没有对x做任何改变只是中途调用了add3,4 为什么打印x是7 因为x接收的是add函数返回的c的引用x和c其实是一个位置的同一变量 x其实就是c所以第二次c改变了x也是跟着改变x7
上面的3和7都是建立在vs这个编译器在函数栈帧销毁后没有清除原先变量空间的前提下如果是不同编译器打印情况应该如下
这里为什么第二次调用addx和c还是同一个位置的同一变量 因为你再一次创建函数栈帧函数栈帧还是那个函数栈帧函数里的变量位置也还是不变的 如下代码所示
void func()
{int c 0;cout c endl;
}
int main()
{func();func();return 0;
}再给大家看一个神奇的东西
void func1()
{int c 0;cout c endl;
}void func2()
{int a 0;cout a endl;
}
int main()
{func1();func2();return 0;
}并不是说同一个函数才能用同一个空间不同函数也是用的一个同一块空间 只不过说不同函数它可能里面变量个数不同它占用同一块空间的大小不同。
可以发现我们虽然上面这样写但是其实这个代码是有逻辑上的危险的 那什么时候可以用引用返回是安全的呢你要么用静态的变量或者栈上的你malloc的。简而言之就是出了作用域那个变量不会销毁那你用引用就是安全的。
对于静态变量我们来看一个例子
int add(int a, int b) {static int c a b;return c;
}
int main()
{int xadd(1,2);cout x endl;add(3, 4);cout x endl;return 0;
}这里x是c的别名c又是一个局部静态变量局部静态变量只会被初始化一次 第一次调用add1,2我们初始化了局部静态变量c为3
第二次调用add3,4由于局部静态变量c已经被初始化过了 所以第二次是不执行static int c a b;的
所以我们的x也就是c的别名还是3
再来看另外一种情况
int add(int a, int b) {static int c 0;c a b;return c;
}
int main()
{int xadd(1,2);cout x endl;add(3, 4);cout x endl;return 0;
}如果这样写我们局部静态变量只会被初始化一次但是cab是每次都要调用的 所以我们第一次打印了3第二次打印了7。 看似两个情况是相同的代码其实内在是完全不同的。
6.3传值、传引用效率比较
以值作为参数或者返回值类型在传参和返回期间函数不会直接传递实参或者将变量本身直 接返回而是传递实参或者返回变量的一份临时的拷贝因此用值作为参数或者返回值类型效 率是非常低下的尤其是当参数或者返回值类型非常大时效率就更低。 为什么能提高效率大白话就是如果你传值返回如果那个值是个特别大的结构体那返回消耗的代价肯定也很大。但如果你传引用返回其实就和返回一个指针差不多占用空间会小很多的。
6.4常引用 如果一个变量已经被const修饰了也就说它已经不能被修改了 如果这时你还对常变量取别名是有问题的。 因为我常变量a本身已经不能修改了但是你给我取个别名b还能修改这就产生矛盾了。
所以如果想给常变量取别名别名也必须是const修饰的如下图这样就不会报错了
另外虽然说const修饰的变量必须用const修饰的别名
但是没有const修饰的变量也可以用const修饰的别名 有点类似于权限不能放大但是权限可以缩小 还有一个需要提醒的点这个大家应该不说也清楚。 就是不同类型的引用也是不可以互相用的 比如这里j是doible类型的你可以把i强转赋值给j 但是rj是double类型的引用它只能是double类型变量的别名你int类型的i就不可以用rj这个别名
进一步的如果我们在double前加const这里又不报错了。 这是因为我们在强转的时候会默认生成一个临时变量通过把int类型的先变成double类型的临时变量再把临时变量赋给double类型的变量。
而生成的临时变量又具有常变量的性质所以我们const double rji是可以的。相当于先产生一个const double类型的临时变量再把这个临时变量赋给rj。
6.5引用和指针的区别
语法上我们c的引用就是原来的变量它们共享一块空间 但是底层实现其实引用和指针是一样其实是开辟了额外空间的
int main()
{int a 10;int b a;//语法上b没有开额外空间b就是aa就是b//但是底层实现其实开了空间return 0;
}下面是反汇编的部分截图
所以引用底层实现就是指针但是我们语法上默认c的引用就是和原先变量共享一块空间。
举个例子
int main()
{char ch x;char c ch;cout sizeof(c) endl;return 0;
}按道理这里c其实底层是个指针但是我们语法上还是默认引用就是原先变量 所以这里sizeofc其实就是sizeof(ch)也就是1 引用和指针的不同点: 引用概念上定义一个变量的别名指针存储一个变量地址。 引用在定义时必须初始化指针没有要求 引用在初始化时引用一个实体后就不能再引用其他实体而指针可以在任何时候指向任何 一个同类型实体 没有NULL引用但有NULL指针 在sizeof中含义不同引用结果为引用类型的大小但指针始终是地址空间所占字节个数(32 位平台下占4个字节) 引用自加即引用的实体增加1指针自加即指针向后偏移一个类型的大小 有多级指针但是没有多级引用 访问实体方式不同指针需要显式解引用引用编译器自己处理 引用比指针使用起来相对更安全 ps引用不是百分百安全的比如前面讲的你对出了某个作用域就销毁的变量进行引用就会产生问题。
七、内联函数
以inline修饰的函数叫做内联函数编译时C编译器会在调用内联函数的地方展开没有函数调 用建立栈帧的开销内联函数提升程序运行的效率
你就简单记住写函数的时候在返回值类型前加一个inline就可以提升效率。其他的和正常写函数没啥区别
inline int add(int x, int y) {int z x y;return z;
}
int main()
{int x add(1, 2);cout x endl;return 0;
}ps内联更适合小函数如果是那种代码比较多的大函数或者递归的函数用内联就不合适。就算你大函数加内联编译器也会默认大函数没有加内联。
八、auto关键字
auto可以自动推倒类型
int main()
{int a 0;int b a;auto c a;//auto可以自动推倒类型这里把int型的a赋给c那么就默认c是int型auto d a;//指针可以显示的写也可以不显示的写auto* e a;auto f a;//引用必须显示的写f;cout typeid(c).name() endl;//typeid(变量名).name()可以用于打印一个变量的类型cout typeid(d).name() endl;//d是int型a的指针所以打印int *cout typeid(e).name() endl;//指针可以显示的写也可以不显示的写所以这里auto da和auto*ead和e的类型是一样的cout typeid(f).name() endl;//f是a的引用f和a的类型一样都是intreturn 0;
}但是上面的代码说实话没有体现出auto真正的意义它真正的作业需要到vector顺序表那块知识才能体现出来。比如下面这段代码定义对象时类型较长用auto比较方便但是auto不能作参数也不能做返回值有些比较新版本支持做返回值但是强烈不建议做返回值你返回一个值都不知道是什么类型我怎么知道用什么接收 auto可以让一个很长的定义变得很短哈哈哈哈这段代码如果暂时看不懂没关系后面会详细讲
另外auto是不能用来声明数组的别问为什么当时写c这么语言的创始人是这么规定的。
九、基于范围的for循环
9.1范围for的语法
在C98中如果要遍历一个数组可以按照以下方式进行
int main()
{int arr[] { 1,2,3,4,5 };//对数组的三种遍历方法//法一for (int i 0;i sizeof(arr) / sizeof(arr[0]);i) {arr[i] * 2;}//法二for (int* p arr;p arr sizeof(arr) / sizeof(arr[0]);p) {cout *p ;}cout endl;//法三for (auto e : arr) {//会依次取数组arr中的数据赋给e对象自动判断结束自动循环往后走cout e ;}
}需要注意的是法三这个是对赋值后的e进行打印并没有对原先的数进行更改比如
int main()
{int arr[] { 1,2,3,4,5 };for (auto e : arr) {e;cout e ;}cout endl;for (auto e : arr) {cout e ;}
}当然了如果你是用引用来接收数组数据那么数组中的元素是会跟者变化的
int main()
{int arr[] { 1,2,3,4,5 };for (auto e : arr) {e;cout e ;}cout endl;for (auto e : arr) {cout e ;}
}9.2 范围for的使用条件
for循环迭代的范围必须是确定的 对于数组而言就是数组中第一个元素和最后一个元素的范围对于类而言应该提供 begin和end的方法begin和end就是for循环迭代的范围。 注意以下代码就有问题因为for的范围不确定
void TestFor(int array[])
{for(auto e : array)cout e endl;
}迭代的对象要实现和的操作。(关于迭代器这个问题以后会讲现在提一下没办法 讲清楚现在大家了解一下就可以了)
十、指针空值nullptr void func(int) {cout f(int) endl;
}void func(int*) {cout f(int*) endl;
}
int main()
{int* ptr NULL;func(0);//调用int那个函数func(NULL);//这里依旧是调用了int那个函数//NULL实际是个宏在传统c语言头文件stddef.h中NULL本质就是0//这个是当时写这个库的时候那个创作者定义的func(ptr);//调用int*那个函数func(nullptr);//调用int*那个函数//我们一般初始化空指针也是用nullptrreturn 0;
}