您好,登录后才能下订单哦!
C++的初步学习有以下几个方面
我们知道,在c中有32个关键字,而c++中有63个关键字
分别为
为什么会有命名空间,他的作用是什么?
在一个大的工程里,要定义很多变量和函数,若将这些变量和函数都定义在全局作用域中,一不小心就可能出现重复定义的情况。因而引入命名空间的概念,其目的是对标识符名称进行本地化,以避免命名冲突或名字冲突。
命名空间是什么?
一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间里。命名空间里可有变量、函数、结构体、另一个命名空间等等普通在全局定义的命名空间里都可以有。在不同的命名空间里可以使用一个变量名。以后在使用某个命名空间里的某个变量,引入就可以了。这样定义变量时,就不用考虑之前这个名字有没有用过,只用看在这个命名空间里存不存在该变量。
命名空间的定义
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。命名空间的定义有以下三种形式:
//1.普通定义
namespace N1 // N1为命名空间的名称
{
int a;
int Add(int left, int right)
{
return left + right;
}
}
//2.嵌套定义
namespace N2
{
int a;
int b;
int Add(int left, int right)
{
return left + right;
}
namespace N3
{
int c;
int d;
int Sub(int left, int right)
{
return left - right;
}
}
}
//3.重复的定义
namespace N1{int a};
namespace N1{int b};
//在编译时,编译器会自动将其合并为一个命名空间,在定义的时候也可将其看做同一个命名空间,因而同名命名空间不要使用相同变量
命名空间的使用
在命名空间里定义的内容是不可以直接使用的。
引用一个操作符 ‘::’ 作用域限定符用于在作用域外引用作用域里的内容
引用一个关键字:在一个作用域中使用 using 将另一个命名空间里的想要的内容拿出来,方便下面使用
使用方式有以下三种:
//1.加命名空间名称及作用域限定符
namespace N
{
int a;
int b;
}
int main{
printf("%d\n", N::a); 打印N中的a
return 0;
}
//2.使用using将命名空间中成员引入
using N::b;
int main()
{
printf("%d\n", N::a); //并没引入a
printf("%d\n", b); //在此的b就可以直接使用了
return 0;
}
// 3.使用using namespace 命名空间名称引入
using namespce N; //将N 中所有的内容都引入
int main()
{
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
输出函数:cout标准输出(控制台)类似于printf
输入函数:cin标准输入(键盘)类似于scanf
两个函数属于标准库 iostream 再引入命名空间std
用法:他们的用法比printf和scanf要灵活,输出不用再加%d..来说明输出/输入什么类型的值,可连接各种类型的值
例如如下代码
#include <iostream>
using namespace std;
int main()
{
int a;
double b;
char c;
cin>>a;
cin>>b>>c;
cout<<a<<endl;
cout<<b<<" "<<c<<endl;
return 0;
}
概念:缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。例如:
void TestFunc(int a = 0)
{
cout<<a<<endl;
}
int main()
{
TestFunc(); // 没有传参时,使用参数的默认值 0
TestFunc(10); // 传参时,使用指定的实参
}
在一个函数的形参列表中,我们可以给一部分形参默认值,也可以全给。因此分为半缺省参数和全缺省参数,用法及要求如下
全缺省参数:每个形参都赋了缺省值
void TestFunc(int a = 10, int b = 20, int c = 30)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
int main()
{
TestFunc(); //10 20 30
TestFunc(1); //1 20 30
TestFunc(1,2); // 1 2 30
//为什么把1给a呢?我们从半缺省参数用法里找答案
}
半缺省参数:不是所有的形参都赋了缺省值,但赋半缺省参数有一定规则: 半缺省参数必须从右往左依次来给出,不能间隔着给,就是前面的可以省略,但一旦给值,后面的都必须都给值 。因此
void TestFunc(int a, int b = 10, int c = 20)√
void TestFunc(int a=10, int b , int c = 20) ×
void TestFunc(int a=10, int b=20 , int c ) ×
通过半缺省参数的规则,我们可回答为什么全缺省参数给值是从前往后给的:半缺省参数前面的可以省略,所以在不知道函数是不是半缺省参数的情况下,实参要赋从第一个形参开始赋值
定义:在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题。
如以下代码:
int Add(int left, int right)
{
return left+right;
}
double Add(double left, double right)
{
return left+right;
}
long Add(long left, long right)
{
return left+right;
}
int main()
{
Add(10, 20);
Add(10.0, 20.0);
Add(10L, 20L); //通过实参类型来找函数
return 0;
}
注:函数不可仅靠返回值类型来实现重载
short Add(short left, short right)
{
return left+right;
}
int Add(short left, short right)
{
return left+right;
}
//这两个函数无法实现重载
void TestFunc(int a = 10);
void TestFunc( );
//这两个函数就无法形成重载,在另一个函数中调用TestFunc( ),编译器不知道要调用哪一个;
缺省函数与普通函数无法形成重载,例如:
void TestFunc(int a = 10);
void TestFunc(int a );
//这两个函数就无法形成重载,在另一个函数中调用TestFunc(num ),编译器不知道要调用哪一个;
因而:想要形成函数重载,要确保两个函数在调用的时候不会起冲突,不会出现在传某个值的时候,两个函数都可以调的情况。
我们知道:c语言中不可以实现函数重载,为什么c++中可以呢?因为在程序编译时,编译器会对每个函数名进行命名修饰,下面我们来引入命名修饰的概念
在c++程序编译时,编译器为区分各个函数,会将函数、变量名重新改变,使每个函数名成为全局唯一的名称,将参数类型包含在最终的名字中,因而通过形参列表的不同可以将同名函数进行区分,就可保证名字在底层的全局唯一性。
那么c++中具体将名字修改成什么样子了呢?
有如下代码:
int Add(int left, int right);
double Add(double left, double right);
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}
//在vs下,对上述代码进行编译链接,最后编译器报错:
//error LNK2019: 无法解析的外部符号 "double cdecl Add(double,double)" (?Add@@YANNN@Z)
// error LNK2019: 无法解析的外部符号 "int __cdecl Add(int,int)" (?Add@@YAHHH@Z)
通过上述错误可以看出,编译器实际在底层使用的不是Add名字,而是被重新修饰过的一个比较复杂的名字,被重新修饰后的名字中包含了:函数的名字以及参数类型。
visual stdio 下c++的修饰规则:
通过以上签名及修饰后的名字可推得命名方式:
修饰后名字由“?”开头,接着是函数名由“@"符号结尾的函数名:后面跟着由“@"结尾的类名“C”和名称空间“N”,再一个“@”表示函数的名称空间结束:第一个“A”表示函数调用类型为“_ cdecl” ,接着是函数的参数类型及返回值,由“@”结束,最后由“Z”结尾。其中A后面第一个是返回值类型,然后接下来到@之前都是形参的类型,H表示int,M表示float
那为什么c语言中,同名函数为什么不能构成重载呢?
因为c语言中的名字修饰只是在函数名前加了个下划线,形参列表并未参与名字修饰,因而不能够通过形参列表来区分各个同名函数。
在某个函数前加extern “C”,可将c++工程中某些函数按c的风格来编译
概念:给变量取了个别名,和变量共用一块内存空间,可以通过引用来改变变量。
定义:类型& 引用变量名=引用实体
注意:引用类型必须和引用实体的类型必须相同。
如:
int a = 10;
int& ra = a;//定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra); //结果相同
引用特性:
1>引用在定义时必须初始化,不能存在空着的引用
int& ra ;//会发生错误
//起了外号,这个外号又不是任何人的,这个外号存在有什么意义?
2>一个变量可有多个引用(一个人可以起很多个别名)
3>引用一旦引用一个实体,再不能引用其他实体
int a=0;
int b=1;
int& ra=a;
ra=b; //ra不是改变了引用,只是将b的值赋给ra
printf("%d",a); //->1
常引用
const int a = 10;
int& ra = a; // 该语句编译时会出错,a为常量
//const修饰的变量,引用前也要加const,若不加,那么就可以通过引用修改变量的值了。
const int& ra = a;//正确写法
int& b = 10; // 该语句编译时会出错,10为常量
//引用不能做常数的引用,要引用前面加const,常熟也是不能够被修改的
const int& b = 10;
double d = 12.34;
int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;//这个是正确的的,但rd并不是d的别名
//而是先通过a来形成一个临时变量存放a的整数部分,然后ra引用这个临时变量。但是该临时变量不知道名字,也不知道地址,因而也修改不了,该临时变量具有一定的常性,因而要在ra前加const
引用使用场景
1>做参数:函数形参设为引用类型
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
说明:如果想要通过形参改变实参,可将形参设为普通类型 如果不想要通过形参改变实参,可将形参设为const类型。
效率:传值的效率低于传址、传引用效率。传地址和传引用时间相同。因为传引用和传指针的过程在内存中的变化其实是一样的,传引用的过程在编译时,会转成传指针的形式,在编译过程中,引用是按照指针方式来实现的
#include <time.h>
struct A
{
int a[10000];
};
void TestFunc1(A a)
{}
void TestFunc2(A& a)
{}
void TestRefAndValue()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(int*)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(int&)-time:" << end2 - begin2 << endl;
}
// 运行多次,检测值和引用在传参方面的效率区别
//结果都很小,而且相差无几
//反汇编后,可看到传引用的过程和传指针的过程一模一样。
int main()
{
for (int i = 0; i < 10; ++i)
{
TestRefAndValue();
}
return 0;
}
2>做返回值:将返回值类型设为引用类型
int& TestRefReturn(int& a)
{
a += 10;
return a;
}
注意:如果函数返回时,离开函数作用域后,其栈上空间已经还给系统,因此不能用栈上的空间作为引用类型返回。因此,引用作为返回值,返回变量不应受函数控制,即函数结束,变量的生命周期存在。比如:全局变量,static修饰的局部变量,用户未释放的堆,引用类型参数
发生该错误有以下代码:
int& Add(int a, int b)
{
int c = a + b;
return c;
}
//在函数调用完后,栈上的c占用的那一块空间就被释放了(可以覆盖),因此就没什么意义了
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :"<< ret <<endl;
//->7,Add(3, 4)将c的那一块空间又覆盖掉了
return 0;
}
通过比较,发现传值和指针在作为传参以及返回值类型上效率相差很大,因而可以让引用作为返回值的地方就用引用,除非是要返回一个函数中定义的变量(该变量的空间会随函数调用完而变得无效)要返回值外,其他情况都可用引用返回。
#include <time.h>
struct A
{
int a[10000];
};
A a;
A TestFunc1()
{
return a;
}
A& TestFunc2()
{
return a;
}
void TestReturnByRefOrValue()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
// 测试运行10次,值和引用作为返回值效率方面的区别
int main()
{
for (int i = 0; i < 10; ++i)
TestReturnByRefOrValue();
return 0;
}
引用与指针
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间,但在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
int main()
{
int x = 10;
int& rx = x;
rx = 20;
int* px = &x;
*px = 20;
return 0;
}
对于该代码我们来看反汇编代码:
可发现,在内存中两者在底层的使用方式是一样的,引用也是按照指针方式来实现的
那两者又有什么不同呢?
1> 引用在定义时必须初始化,指针没有要求。因而指针需要判空,而引用不用,因为引用定义时就初始化了
2> 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
3> 没有NULL引用,但有NULL指针
4>在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
5>引用自加即引用的实体增加1,在连续的空间中指针自加即指针向后偏移一个类型的大小
6>有多级指针,但是没有多级引用
7> 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
8> 引用比指针使用起来相对更安全。
概念:以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
普通函数会进行压栈形成栈帧等操作
而内联函数在编译时会直接将调用函数换为函数内部的操作
查看方式:1. 在release模式下,查看编译器生成的汇编代码中是否存在call Add2. 在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不会对代码进行优化,给出vs2013的设置方式):功能->属性->配置->c/c++->将常规中的调试信息格式改为程序数据库,再将优化中的内联函数扩展改为只适用于_inline
特性
1> inline是一种以空间换时间的做法。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。
2>inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。
3>inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。因而内联函数具有文件作用域,只在本文件有用,其他文件不可用。
// F.h
#include <iostream>
using namespace std;
inline void f(int i);
// F.cpp
#include "F.h"
void f(int i)
{
cout << i << endl;
}
// main.cpp
#include "F.h"
int main()
{
f(10);
return 0;
}
// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl f(int)" (?
f@@YAXH@Z),该符号在函数 _main 中被引用
内联函数与const、宏
在c++中,const修饰的变量有常量的特性也有宏的特性,在编译时会发生替换和检测,即使通过指针修改也无法改变变量值。有如下代码
const int a=1;
int *pa=(int *)a;
*pa=2;
printf("%d,%d",*pa,a);
//结果为2,1 a仍然没有修改
而在c中是可以的,因为c中是不会检测的,通过指针也是修改const变量的
宏是在预处理时替换的,不参与编译,也不可调试。
宏的优点:增强代码的复用性。提高性能。
缺点:
1>不方便调试宏。(因为预处理阶段进行了替换)
2>导致代码可读性差,可维护性差,容易误用。
3>没有类型安全的检查 。
因此在c++中,可通过const来代替宏对常量的定义,用内联函数来代替宏对函数的定义
内联函数的优缺点:
https://mp.csdn.net/mdeditor/101083065#
概念:在C++中,auto作为一个新的类型指示符来定义变量,auto声明的变量是由编译器在编译时期推导而得,变量被赋值什么类型,由初始化的值而定。
特性:
1>使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。
2>auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl; //int
cout << typeid(c).name() << endl; //char
cout << typeid(d).name() << endl; //int
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}
使用方法
1>auto与指针和引用结合:用auto声明指针类型时,用auto和auto* 没有任何区别,但用auto声明引用类型时则必须加&.
int x = 1;
auto px = &x;
auto *ppx = &x;
auto& rx = x;
auto rrx = x;
cout << typeid(px).name() << endl;
cout << typeid(ppx).name() << endl;
cout << typeid(rx).name() << endl;
cout << typeid(rrx).name() << endl;
rx = 3;
cout << x << endl; //x发生了变化说明是引用
rrx = 2;
cout << x << endl; //x未发生变化,说明不是引用
2>auto在同一行定义多个变量,当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto f = 1, g = 2;
//auto h = 1, i = 2.3; //编译会报错,h和i类型不同
3>auto不能直接用来声明数组
int h[] = { 1, 2, 3 };
//auto t[] = { 4,5,6 };//编译时会发生错误
为什么要引入这个概念?
对一个有范围的集合由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。
用法:for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
int arr[] = { 1, 2, 3, 4, 5 };
for (auto& e : arr) //=>for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
e *= 2;
for (auto e : arr) //要对元素值进行改变,变量前要加&,不改变,直接普通变量
cout << e << " ";
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
概念:nullptr指针空值常量,表示指针空值使用nullptr。
为什么要有nullptr,NULL为什么无法用于表示空指针了?
在指针定义时,要初始化(否则会出现野指针),在c中用NULL来给一个没有指向的指针,但其实NULL是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量,所以在传空指针时,会出现一些差强人意的错误,如下:
void f(int)
{
cout<<"f(int)"<<endl;
}
void f(int*)
{
cout<<"f(int*)"<<endl;
}
int main()
{
f(0);
f(NULL); //变成0了,进了第一个函数,但我们NULL想表示指针本是想进入第二个函数
f((int*)NULL);
return 0;
}
因而用nullptr来代替C中NULL在指针中的用法。
并且nullptr也是有类型的,其类型为nullptr_t,仅仅可以被隐式转化为指针类型,nullptr_t被定义在头文件中:typedef decltype(nullptr) nullptr_t;
注意:
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。