博客主页:一个努力敲代码的小粉象
系列专栏:《c++初阶》
:苟日新,日日新,又日新。
感谢大家的点赞 评论收藏
前言
今天我们正式开始c++的零基础学习,相信很多学习计算机的同学们都会或多或少对c语言和c++之间的区别有一定的兴趣,但苦于知识琐碎,无法及集中整理,本篇博客将从零基础讲起,将c++和c语言语法的不同一一列举。
c++是在c的基础上,融入进去了面向对象编程思想,并增加了许多有用的库,以及编程范式等等。本博客的主要目标:
补充c语言语法的不足,以及c++是如何对c语言设计不合理的地方进行优化的,比如:IO方面,函数方面,指针方面,宏方面等等。
命名空间
在c/c++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
这里,我们来看一看c语言出现的命名冲突的问题:
这里为什么会失败呢?是因为我们定义的变量rand和头文件中的函数出现了冲突,导致了失败。
所以,c++为了弥补c语言的命名冲突问题,定义了命名空间。
命名空间定义
定义命名空间,需要使用的namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。
1.注意,命名空间中可以定义变量、函数、类型
namespace FBL
{
int rand=0;
int Add(int left,int right)
{
return left+left;
}
sturct Node
{
struct Node* next;
int val;
}
}
2.命名空间可以嵌套
namespace N1
{
int a;
int b;
int Add(int left,int right)
{
return left+right;
}
//嵌套
namespace N2
{
int c;
int d;
int sub(int left,int right)
{
return left-right;
}
}
}
3.同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
4.注意,一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中。
命名空间的使用
我们现在来看看,直接对命名空间中的变量进行引用,会出现什么呢?
我们发现,这里出现了编译的报错。
所以我们在使用命名空间时是不能直接进行引用的,接下来我们将介绍以下三种方式:
-
加命名空间名称及作用域限定符 ( :: )
int main() { printf("%d\n",N::a); return 0; }
-
使用using将命名空间中某个成员引入
using N::b; int main() { //printf("%d\n",N::a); printf("%d\n",b); return 0; }
注意,该方式是指定展开,在工程中使用更加适合。
-
使用using namespace命名空间名称引入
using namespace N;
int main()
{
//printf("%d\n", N::a);
//printf("%d\n", b);
int add=Add(10, 20);
cout << add << endl;
}
c++输入&输出
我们在学习c语言的过程中,输入和输出函数分别是scanf和printf这两个函数,但在c++中,我们要引入新的符号:cout和cin。
先来看看Hello,world!用c++该怎么实现吧。
感受来自c++的初次问候吧!!
#include<iostream>
using namespace std;
int main()
{
cout<<"Hello,world!"<<endl;
reutrn 0;
}
说明:
1.使用**cout标准输出对象(控制台)和cin标准输入对象(键盘)**时,必须包含头文件,以及按命名空间使用方法使用std;(类比c语言当中的stdio.h)
2.cout和cin是全局的流对象,endl是特殊的c++符号,表示换行输出,他们都包含在头文件中。
3.<<是流插入运算符,>>是流提取运算符。
4.cout/cin的输入输出更加的方便,不需要像printf/scanf一样,需要手动控制格式。c++的输入输出可以自动识别变量类型。
5.实际上cout和cin分别是ostream和istream类型的对象,>>和<<也设计运算符重载的知识,这个知识我们在后续的博客当中还会着重讲解,这里我们先简单学习他们的应用。
这里还要注意一个问题,早起标准库将所用功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和c头文件进行区分,也为了正确使用命名空间,规定c++头文件不带.h;旧编译器(vc 6.0)中还支持着<iostream.h>的格式,而后续的编译器已不支持。
这里我们来一一实现一下上文当中提到的cout和cin,来看看c++的输入和输出的优越性。
int main()
{
int a;
double b;
char c;
//可以自动识别变量的类型
cin >> a;
cin >> b >> c;
cout << a << endl << b << endl << c << endl;
return 0;
}
std命名空间的使用惯例
std是c++标准库的命名空间,如何展开std使用更合理呢?
1.在日常练习当中,建议直接使用using namespace即可,这样就很方便。
2.using namespace std展开,标准库就会全部暴露出来,如果我们定义跟库重名的类型/对象/函数,就存在冲突问题。该问题在日常练习当中很少出现,但是项目开发中代码较多、规模较大,就会容易出现,所以建议在项目开发中使用指定命名空间+using std::cout展开常用的库对象/类型等方式。
缺省函数
缺省函数概念
缺省函数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
void Func(int a=0)
{
cout<<a<<endl;
}
int main()
{
//没有传参时,使用参数的默认值
Func();
//传参时,使用指定的实参
Func(10);
return 0;
}
缺省参数的分类
-
全缺省参数
void Func(int a = 0) { cout << a << endl; } int main() { //没有传参时,使用参数的默认值 Func(); //传参时,使用指定的实参 Func(10); return 0; }
-
半缺省参数
void Func(int a, int b = 10, int c = 20) { cout << "a= " << a << endl; cout << "b= " << b << endl; cout << "c= " << c << endl; } int main() { Func(15,30); }
-
半缺省参数必须从右往左依次来给出,不能间隔给,容易引发歧义。
-
如果声明和定义中同时出现缺省参数,而恰巧两个位置提供的值是不同的,那么编译器就无法确定倒一该用哪个缺省值,所以:缺省参数不能在函数声明和定义中同时出现,一般缺省参数在声明中出现一次即可。
注意:
1.缺省值必须是常量或者全局变量
2.c语言是不支持缺省参数的。
函数重载
自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。比如:以前有一个笑话,国足是“谁也赢不了。”兵乓球是“谁也赢不了。”
函数重载的概念
函数重载:是函数当中的一种特殊情况,c++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数或类型或者类型顺序)不同,常用来处理实现功能类似数据类型的不同问题。
这里我们要强调一下,函数重载无非满足两个条件其一,函数名相同,功能相似,其二,参数不同,其中又包括类型不同、个数不同而与返回值无关
下面我们通过代码来感受其中的不同
//1.参数类型不同
int Add(int left, int right)
{
return left + right;
}
double Add(double left, double right)
{
return left + right;
}
//2.参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
至于为什么c++支持函数重载的原理,这与程序的编译、汇编、连接的编译过程有关,我们将会在之后的博客中向大家解答,敬请期待~~~~
引用
引用概念
引用不是新定义一个变量,而是给已存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
这里我们进行一个形象的比喻~~
李逵,在家被称为铁牛,江湖上人称黑旋风。
再用图像辅助大家理解~~~
我们先在内存中开辟了一个空间a,然后又给这个一块空间取了一个别的名字,b。
所以,无论是a还是b,指向的均是这一块空间,a和b的地址是相同的。
类型& 引用变量名(对象名)=引用实体
void TestRef()
{
int a = 0;
int& ra = a;// 定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra);
}
int main()
{
TestRef();
return 0;
}
PS:引用类型必须和引用实体是同一种类型的
引用特性
-
引用在定义时必须初始化,否则,
-
一个变量可以有多个引用
-
引用一旦引用一个实体,再不能引用其他实体。(这里就说明了,在c++中,引用不能改变指向)
void TestRef() { int a = 0; //int& ra;//这里会出现编译错误,引用必须要初始化 int& ra = a; int& rra = a; printf("%p %p %p\n", &a, &ra, &rra); } int main() { TestRef(); return 0; }
我们可以看到,上述的a,ra和rra都是同一个地址,也就是说明一个变量可以有多个别名。
常引用
我们先复习一个关键字const,const用来定义常量,如果一个变量被const修饰,那么他的值就不可以被改变。
-
权限的放大,编译错误
void TestConstRef() { const int a = 10; int& ra = a; } int main() { TestConstRef(); }
这里其实也很好理解,进行一个类比,比如李逵的妈妈不允许李逵去喝酒,结果看到李逵去喝酒,气哄哄地问李逵为啥去喝酒,李逵说:"不让喝酒的是李逵,现在在喝酒的是铁牛。"大家是不是感觉到了很荒唐?所以,权限的放大就会导致编译错误。此时,我们在int&前加上const进行权限的限制,便没有问题了。
-
权限可以缩小,编译没有问题
void TestConstRef() { int a = 10; const int& b=a; } int main() { TestConstRef(); }
这里大家也可以去类比一下**”李逵喝酒“**这个例子,能很快就想通~~
-
类型不同,编译错误
void TestConstRef() { double a = 13.14; int& ra = a; } int main() { TestConstRef(); }
这里很好理解,引用的类型必须和实体相同
但是,我们来看看下面这个代码~~~
void TestConstRef()
{
double a = 13.14;
const int& ra = a;
}
int main()
{
TestConstRef();
}
这里大家是不是会大吃一惊,为什么加上一个const就会成功了呢?
下面我们就来讲讲这其中的奥妙~~
首先,我们先要明确两个可能会被遗漏的知识点:
其一,在进行类型转换(强制类型转换,隐式类型转换)的时候,是会将变量储存到临时变量中;
其二,临时变量具有常性。
下面我们还是通过图像来辅助理解一下~~~
也就是说,在进行类型转换的时候,原本的数据类型是不发生改变的,而是拷贝出一份临时变量,将临时变量的类型进行改变,而拷贝出的临时变量是具备常属性的,所以需要在前加上const来表示其常属性。
使用场景
-
做参数
我们还记得,当初学习c语言的时候,强调的形参和实参的经典问题,形参是实参的拷贝,单纯交换形参的值,是没有改变实参的,所以我们选择了传址的方法进行交换。而今天,我们在c++中实现两个数据的交换,则用到了引用,通过刚刚的了解我们知道了引用和实体都是指向同一块位置,所以使用了下面这个代码实现~~~
void Swap(int& left, int& right) { int tmp = left; left = right; right = tmp; } int main() { int a = 3; int b = 4; Swap(a,b); cout << a << endl << b << endl; }
-
做返回值
int count() { int n = 0; n++; return n; } int main() { int ret = count(); cout << "ret= " << ret << endl; return 0; }
这个情形我们在C语言的学习中经常见到,但是这里就要提出一个疑问了——这里的返回值是n本身还是一个关于n的拷贝呢?
这里就需要联系函数栈帧来学习了。
这里我们发现,当count函数结束之后,函数栈帧就会被销毁,其中n返回值其实是n的一个拷贝。
那么我们这里发生一点点变化,返回值用引用,又会发生什么呢?
这里我们发现出现了一个warning警告。
那么这个程序是一定无法运行了吗?
答案是不一定,取决于是否清理函数栈帧
如果count函数已经被销毁,那么这时ret指向的位置将为一个随机值(这里可以类比c语言当中的野指针),如果count函数没有被销毁,原本的n开辟的空间还存在,那么此时ret指向的位置就是n的位置。
那么我们再来看一个代码,可能又会让老铁们摸不着头脑了~~~
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1,2);
cout << "ret= " << ret << endl;
Add(3, 4);
cout << "ret= " << ret << endl;
return 0;
}
咦?第二次并没有将Add中的c赋值给ret,为什么ret的值也发生改变了呢?
这里我们还是通过图像进行理解~~
引用和指针的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
但是在底层的实现上实际上是有空间的,因为引用是按照指针的方式来实现的。
引用和指针的不同点
1.引用概念上定义一个变量的别名,指针存吃一个变量地址。
2.引用在定义上必须初始化,指针则没有具体的要求。
3.引用在初始化时引用一个实体之后,就不能再引用其他实体,而指针可以在任何的时候指向任何一个同类型的实体。
4.没有NULL引用,但是有NULL指针。
5.在sizeof中的含义不同,引用结果为引用类型的大小,但是指针始终是地址空间所占字节数
6.引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7.有多级指针,但是没有多级引用。
8.访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
9.引用比指针使用起来相对更加安全。
内联函数
在我们c语言的学习过程中,我们学过一个概念,叫做宏替换,大家还记得宏替换的相关知识点吗?宏替换需要注意的坑有很多,不好控制。比如,我们写一个Add(x,y)的宏替换,应该怎么写呢?可能
概念
以inline修饰的函数叫做内联函数,编译时c++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内敛函数提升程序运行的效率。
(这里简要介绍以下,call的含义其实也就可以理解为是为函数开辟了新的栈帧,所以ADD括号内的就是开辟的新的函数站真的地址)
我们发现,这里在调用内联函数inline时,并没有开辟新的函数栈帧,而是进行了函数的展开,证明了inline的作用。
特性
1、要注意,inline是一种用空间换时间的做法,如果编译器将函数当成内联函数处理,那么在编译阶段,会用函数体替换函数调用,缺陷,可能会是目标文件变大;优势,减少了调用函数的开销,提高程序的运行效率。
2、inline对于编译器而言只是一个建议,不同的编译器关于inline实现机制可能不同,一般建议:函数规模较小,不是递归或者频繁调用的函数,采用inline进行修饰。
3、inline不建议声明和定义分离,分离会导致连接错误。因为inline被展开,就没有函数地址了,链接就会找不到。(也就是说如果inline声明和定义分离,那么就只能在当前文件进行使用)
auto关键字(c++11)
概念
auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器时期推导而得。(简单来说,就是auto可以自动推导变量的类型是什么,不用再像c语言当中,需要对变量进行类型的表述)
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
}
注意
**使用auto定义变量时,必须对其进行初始化,否则在编译阶段编译器无法根据初始化表达式推导auto的实际类型。**因此,auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期间会将auto替换为变量的实际类型。
auto的使用细则
1.auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时必须加&。
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
return 0;
}
2.在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0;
}
auto不能推导的场景
1.auto不能作为函数的参数,也不能作为返回值
2.auto不能直接用来声明数组
基于范围的for循环
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会犯错误。因此c++引入了基于范围的for循环。for循环后的括号由冒号“:”,分为两个部分:第一个部分是范围内用于迭代的变量,第二个部分则表示被迭代的范围。
void TestFor()
{
int a[] = { 1,2,3,4,5 };
for (auto& e : a)
{
e *= 2;
}
for (auto e : a)
{
cout << e << " ";
}
return;
}
int main()
{
TestFor();
}
指针空值nullptr
我们在c语言中,用NULL表示空指针,但在c++当中,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量,所以,在c++中,我们引入了新的符号nullptr。所以,为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
总结
我们今天从命名空间、c++的输入输出、缺省函数、函数重载、引用、内联函数、auto关键字、基于范围的for循环、空指针nullptr这几个大的方面,详细阐述了C语言与c++的区别,帮助老铁们实现c语言到c++的顺利过渡,不知道老铁们学废了嘛~~
学习编程语言,练习是必不可少的,基础知识只是最基本的东西,我与诸君共勉,不仅要认真总结基础知识点,更要重视实操,不然也只是徒劳无功~~~~
本人水平有限,难免出现知识点遗漏、错误的情况,愿各位大佬批评指正,不胜欣喜。都看到这里了,不点赞、评论、收藏鼓励三连再走嘛,感谢您的观看!!!