C++入门学习

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++入门学习相关的知识,希望对你有一定的参考价值。

C++的初步学习有以下几个方面

1.C++关键字

我们知道,在c中有32个关键字,而c++中有63个关键字
分别为
技术图片

2.命名空间

为什么会有命名空间,他的作用是什么?
在一个大的工程里,要定义很多变量和函数,若将这些变量和函数都定义在全局作用域中,一不小心就可能出现重复定义的情况。因而引入命名空间的概念,其目的是对标识符名称进行本地化,以避免命名冲突或名字冲突。
命名空间是什么?
一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间里。命名空间里可有变量、函数、结构体、另一个命名空间等等普通在全局定义的命名空间里都可以有。在不同的命名空间里可以使用一个变量名。以后在使用某个命名空间里的某个变量,引入就可以了。这样定义变量时,就不用考虑之前这个名字有没有用过,只用看在这个命名空间里存不存在该变量。
命名空间的定义
定义命名空间,需要使用到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 N1int a;
namespace N1int 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; 

3.C++输入&输出

输出函数: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;

4.缺省参数

概念:缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。例如:

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 )  ×

通过半缺省参数的规则,我们可回答为什么全缺省参数给值是从前往后给的:半缺省参数前面的可以省略,所以在不知道函数是不是半缺省参数的情况下,实参要赋从第一个形参开始赋值

5. 函数重载

定义:在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题。
如以下代码:

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的风格来编译

6. 引用

概念:给变量取了个别名,和变量共用一块内存空间,可以通过引用来改变变量。
定义:类型& 引用变量名=引用实体
注意:引用类型必须和引用实体的类型必须相同。
如:

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> 引用比指针使用起来相对更安全。

7.内联函数

概念:以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来代替宏对常量的定义,用内联函数来代替宏对函数的定义

8. auto关键字

概念:在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 ;//编译时会发生错误

9. 基于范围的for循环

为什么要引入这个概念?
对一个有范围的集合由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此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循环迭代的范围。

10.指针空值---nullptr

概念: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;

注意:

  1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
  2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同,都是4。
  3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。

以上是关于C++入门学习的主要内容,如果未能解决你的问题,请参考以下文章

[C++]C++入门到入土篇 HelloWorld 解析 && C++入门

C++入门学习

C++跨平台学习:入门了解

《挑战30天C++入门极限》c++中指针学习的两个绝好例子

C++(OI竞赛入门)学习指南一

C++学习书籍:从入门到精通的一套书籍都在这里