c++基础知识

简介

  本文将介绍c++相关基础知识,包括关键字、命名空间、c++输入输出、缺省参数、函数重载、引用、内联函数、auto关键字、基于范围的 for 循环、指针空值—nullptr等知识点。

正文

  首先我们要知道 c++ 和 c 语言的区别,两者都是高级语言。
而 c语言是面向过程,是基于函数编程的;c++是基于面向对象的,它是对 c语言中的一些不合理的地方进行了改进。

  那么现在开始了解一些 c++ 的基础知识:

  1. 关键字
    c++关键字的个数和版本有关系,c++98版本有63个关键字,而 c语言的 c99版本有32个关键字。
    下表列出了c++98版本的各个关键字:
    c++基础知识_第1张图片
  2. 命名空间
      因为在 c++中,变量、函数、类都是大量存在的,这些变量、函数、类都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的就是对标识符的名称进行本地化,以避免命名冲突(名字污染)。命名空间就是为了解决名字冲突(名字污染)的问题的。
      那么命名空间是如何定义的呢?首先要有 namespace 这个关键字。命名空间有两种表示方式:
    1. 普通命名空间
       namespace 命名空间的名字
         {
             成员列表;
         }
       例如:namespace N1
               {
                     int a = 10;
                     int b = 20;
                     int Add(int left, int right)
                     {
                          return left + right;
                     }
               }
    
    命名空间可以放变量名,也可以放函数,结构体
    
    1. 嵌套
      namespace N2
      {
	       int a = 10;
	       int b = 20;
	       int Sub(int left, int right){
	         	return left - right;
	       }
	       namespace N3
	       {
		         int a = 10;
		         int b = 20;
		         int Mul(int left, int right){
		             return left * right;
	        	 }
	       }
      }
  此处要注意的是在同一个工程中可以定义多个相同的命名空间,但是编译器会自行将多个相同名称的命名空间合并成一个
  例如:
      namespace N1
      {
	       int a = 10;
	       int b = 20;
	       int Add(int left, int right)
	       {
	         	return left + right;
	       }
      }
      namespace N1
      {
	       int c = 0;
	       int d = 0;
	       int Div(int left, int right)
	       {
	         if (0 == right)
	             exit(1);
	         	return left / right;
	       }
      }
      //此时N1::会有a,b,Add,c,d,Div六个成员,即合并了两个N1命名空间。

命名空间的使用方法:
方式一:命名空间::成员名字(::是作用域限定符)
方式二:using 命名空间名字::成员名字; //相当于把这个命名空间的名字导出来作为当前工程的全局变量
  例:using N1::a;
  方式二的适用场景:当前命名空间中个别成员多次在某个文件中被使用,但是可能会造成命名冲突,因为当前工程可能本来就有一个全局变量。
方式三:using namespace N1; //将整个命名空间导出来
  方式三的适用场景:当前命名空间中成员在某个文件中应用的比较多,缺陷:冲突率较高


  1. c++输入&输出
    c++的输入是cin >> 内容;
      比如cin >> a; 就是向 a 这个变量中输入值。
    c++的输出是cout >> 内容 >> endl;
      比如cout << a << endl; 就是输出 a 中的内容,endl 是换行的意思。
    要注意的是输入输出需要调用标准库 std,以及包含头文件

    • 可以直接使用using namespace std;
    • 也可以使用using std::cin;…
    • 或者直接在语句中这样写std::cin >> a;
      这三种都是可以的。
  2. 缺省参数
      缺省参数是声明或定义函数时为函数参数指定一个默认值,在调用该函数时,如果没有指定实参就采用这个默认值,否则使用制定的实参。
      可以这样想,缺省参数就相当于备胎一样,对于女神来说,如果女神需要她的男神时(指定实参),备胎(缺省参数)就没有作用了,但是如果女神和他的男神吵架了,这时候备胎就该站出来了,哈哈。
    比如:

    void test(int a = 0)
    {
     	cout << a << endl;
    }
    int main()
    {
         test();	//没有传参,a 就是0
         test(4);	//传了参数,a 就是 4
    }
    

    缺省参数不能同时出现声明和定义中,否则会出现混淆,最好是放在声明的时候。


    缺省参数分类:

    1. 全缺省参数(函数形参必须全部赋值):
    void Test(int a = 1, int b = 2, int c = 3){
        	cout << a << " " << b << " " << c << endl;
    }
    int main()
    {
        	//传参是从左向右传的
            Test(10, 20 ,30);	//输出10 20 30
            Test(10, 20);		//输出10 20 3
            Test(10);			//输出10 2  3
            Test();				//输出缺省参数1 2 3
            return 0;
    }
    
    1. 半缺省参数(部分参数带了缺省值,函数形参要从右往左依次赋值,不可以间隔着赋值)
    void Test1(int a, int b, int c = 3){
        	cout << a << " " << b << " " << c << endl;
    }
    void Test2(int a, int b = 2, int c = 3){
        	cout << a << " " << b << " " << c << endl;
    }
    int main()
    {
            //有几个参数没有缺省值,就必须最少传几个缺省值
            Test(10, 20 ,30);
            Test(10, 20);
            Test(10); //Test1不可以,2可以
            Test();    //两个都不可以
            return 0;
    }
    

还要注意缺省值必须是常量或者全局变量,在 c 语言中是不支持缺省参数的。


  1. 函数重载
      函数重载是函数的一种特殊情况,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; 
     }
    

  函数重载需要注意:它是在程序编译期间对函数的参数进行类型推演,根据推演的结果取选择合适的调用函数,如果没有最佳匹配函数,就尝试去转换,如果匹配不到,或者不明确(就是函数重载的几个函数都可以被调用),那么程序就会报错。

  那么为什么c++能够支持函数重载呢?这就要了解一下名字修饰(name Mangling)

  首先我们要知道在 c/c++中一个程序要运行起来需要经历预处理—>编译—>汇编—>链接。

而名字修饰是一种在编译过程中,将函数、变量的名称重新改编的机制,简单来说就是编译器为了区分各个函数,将函数通过某种算法,重新修饰为一个全局唯一的名称。换句话说,就是在底层给它重新命名以供编译器识别。

  在c语言中,他的名字修饰规则非常简单,只是在函数名前面加了下划线。
我们常见到的error LNK2019: 无法解析的外部符号 _Add,该符号在函数 _main 中被引用。这里面就是给函数Add进行了名字修饰,只是简单的加了下划线。

在c++中,我们在vs中运行下面这段代码:

	int Add(int left, int right);
	double Add(double left, double right);
	int main()
	{
   	 	Add(1, 2);
    	Add(1.0, 2.0);
   	 	return 0;
	}

编译链接后,编译器就会报下面的错误:

error LNK2019: 无法解析的外部符号 “double cdecl Add(double,double)” (?Add@@YANNN@Z)
error LNK2019: 无法解析的外部符号 “int __cdecl Add(int,int)” (?Add@@YAHHH@Z)

由此可以看出,c++的名字修饰规则复杂了许多。

double Add(double left, double right);

double cdecl Add(double,double)" (?Add@@YANNN@Z)

这两者可以看出,名字修饰后有函数的类型,以及参数类型,这里的cdecl是函数调用类型,在名字修饰过的名字中就是第一个A表示的意思;N表示 double 类型,H表示 int 类型,最后以“@”结束,以“Z”结尾。


面试题:

  1. C语言中为什么不能支持函数重载?
      答:因为C语言底层使用的名字修饰规则太简单,只是在函数名前面加上一个下划线 _ ,而C++支持函数重载是因为C++底层会将函数的类型加入名字的修饰中。
      例如:double Add (int left, double right);在C语言底层中,这个函数的名字就是 _Add而在C++底层中,这个函数的名字是 ?Add@@YANHN@Z
  2. C++中函数重载底层是怎么处理的?
      答:编译器会在底层的名字上加上类型修饰,才能实现函数重载。
  3. C++中能否将一个函数按照 C 的风格来编译?
      答:在函数定义前加上extern "C"即可,这就是将代码按照C语言规则来编译。

extern “C”

有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern “C”,意思是告诉编译器,将 该函数按照C语言规则来编译。

	extern "C" int Add(int left, int right);
	int main()
	{
    	Add(1,2);
    	return 0;
	}
  1. 引用
    在 C 语言中函数传参有传值和传址两种方式。
    C++又有一种新的形式—引用,引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
    定义引用变量的格式:
      类型& 名字 = 实体;
      int a = 10;
      int& ra = a;
      ra = 20; //将 ra 的值改变,a 的值也会改变,因为他们是共用同一块内存空间。
    要注意:引用变量的类型必须与其实体的类型一致,否则会报错。


    引用的特性:

    1. 引用变量必须要初始化,不能这样写 int& ra; 需要给 ra 初始化。
    2. 一个变量可以有多个引用。
    3. 引用一旦引用一个实体,就再不能引用其他实体。

    常引用:

    const void TestConstRef() { 
    	const int a = 10; 
    	//int& ra = a;   // 该语句编译时会出错,a为常量 
    	const int& ra = a; 
    	// int& b = 10;  // 该语句编译时会出错,b为常量 
    	const int& b = 10; 
    	double d = 12.34; 
    	//int& rd = d;  // 该语句编译时会出错,类型不同 
    	const int& rd = d; 
    }
    

    常引用有三种写法:
    1)const int& ra = 10;
    2)const int a = 10;
      const int& ra = a; //这种最常用
    3) double d = 12.34;
      const int& rd = d; //此处的 rd 不是对 d 的引用,可以看 rd 和 d 的地址是不同的
    //那么对第三个解释一下:此处本来类型不同,是违背了引用的定义的,但是加上 const 为甚么会可以?是因为 double d = 12.34; 这条语句,会创建一个整型临时变量,然后const int& rd是对这个临时变量的引用。这个临时变量是不知道名字也不知道地址的。所以在后期修改 d 的值是不会改变 rd 的值。

    c++基础知识_第2张图片

    引用的应用场景:

    1. 直接给一个变量取别名
    2. 做函数的参数
    3. 做返回值

  做返回值时,如果以引用类型返回,返回值的生命周期必须不受函数的限制(即比函数生命周期长)。因为如果函数返回时,离开函数作用域后,其栈上的空间已经还给系统,因此不能用栈上的空间作为引用类型返回。
  看下面这段代码:

int& Add(int& left, int& right)
{
	int ret = left + right;
	return ret;
}
int main()
{
	int a = 1;
	int b = 2;
	int& ret1 = Add(a, b);
	int c = 3;
	int d = 4;
	int ret2 = Add(c, d);//执行到这句,ret1的值也会改变成ret2的值,原因见下面
	printf("%d \n", ret2);
	system("pause");
	return 0;
}

c++基础知识_第3张图片
c++基础知识_第4张图片
在这里插入图片描述
c++基础知识_第5张图片
c++基础知识_第6张图片
c++基础知识_第7张图片
  综上,所以注意:如果按照引用的方式作为函数的返回值,不要返回栈上的空间如果要用就要让返回值不受函数影响—返回实体的生命周期比函数的声明周期长(例:用堆上函数的空间、全局变量、静态变量、引用类型变量)
  效率:引用 约等于 传址 > 传值

  • 指针和引用的区别?
    答:看这段代码:
        int main()
        {
            int a = 10;
            int *pa = &a;
            *pa = 20;
            int& ra = a;
            ra = 20;
            system("pause");
            return 0;
        }

  汇编代码及执行过程如下:
c++基础知识_第8张图片
因此,两者在底层处理方式:一模一样
在底层:编译器在底层将引用按照指针的方式来进行处理的
引用实际就是指针 T& 就相当于 T* const
        const T& 相当于 const T* const
在底层,引用变量实际是有空间的
  概念层面:引用就是变量的别名,与其实体公用同一块内存空间,编译器不会为引用变量开辟新的内存空间。
  区别:
  1)引用在定义期间必须初始化,而指针没有要求
  2)引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  3)没有NULL引用(就是引用必须初始化),但有NULL指针
  4)在sizeof中含义不同;引用结果为引用类型的大小,但指针始终是地址空间所占字节数(32位平台下占 4 个字节,某些64位平台 8 个字节,有些64位系统是按32位系统来编译的https://blog.csdn.net/u010452239/article/details/70238105)
  5)引用自加即引用的实体加 1 ,指针自加即指针向后偏移一个类型的大小
  6)有多级指针,但是没有多级引用(有char **p,没有char&& rc = ch; 但是const char&& rrc = ‘c’;这种可以,叫右值引用,是c++11里的
  7)访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  8)引用比指针使用起来相对更安全(不会空引用,不能有空指针)

7. 内联函数
这里先了解下宏,宏有两种:宏常量 和 宏函数
  在 C 语言中,宏定义是在预处理阶段被替换的,假如在宏定义时不小心将整型定义为字符型,那么编译后会报错,而且错误不好排查。
  在 C++ 中,可以使用const关键字来定义常量,代替宏的作用。const 实在编译阶段被替换的,一旦出错,可以较方便的排查错误。
  看下面这段代码:

int main()
{
	const int a = 10;
	int *pa = 100;
	*pa = 100;
	cout << a << endl;
	cout << *pa << endl;
	return 0;
}

在调试窗口可以看到,当改变*pa的值后,a 和 *pa 的值都发生改变,但是打印出来,a 的值却依然是 10.
c++基础知识_第9张图片
这就是因为const定义的常量在编译阶段被替换,所以在打印的时候,a 的值就又被替换为10.

宏函数:
  优点:提高代码运行的效率—在预处理阶段:用宏函数体替换调用位置,没有函数调用的开销因而提高了代码的运行效率。
  缺点:1、因为是在预处理阶段替换,所以不会进行参数类型检测;
     2、代码的复用性不好,容易造成代码膨胀(宏函数展开很多份,源文件越来越大);
     3、不能调试;
     4、造成副作用
内联函数处理:
  在编译器编译阶段,用函数体替换函数调用的位置,少了函数调用压栈以及 战帧创建的时间开销,提高了代码的运行效率特性:
  1. inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使 用作为内联函数。
  2.inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等 等,编译器优化时会忽略掉内联。
  2. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。


  1. auto关键字
      在c++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指 示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
      注意在使用auto关键字定义变量时要对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类 型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变 量实际的类型。

auto的使用规则:
  1) auto声明指针类型时,用auto 和 auto* 没有任何区别,但用auto声明引用类型时必须加&
  2) 在同一行定义多个变量时,这些变量必须是相同的类型(要是整型就都整型,浮点型则全浮点型),否则编译器会报错,因为编译器会根据第一个类型推导后面的其他变量。


  1. 基于范围的for循环
      在c++11中,可以使用基于范围的for循环,for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二 部分则表示被迭代的范围。
      比如:int a[] = {1, 2, 3, 4, 5};
         for(auto& e : a)   //遍历数组每个元素
         e *= 2;       //给数组中每个元素乘以2

  2. 指针空值nullptr
    在c++98中,NULL不是空指针了,而是宏,在传统的C头文件(stddef.h)中,可以看到下面这段代码:

    #ifndef  NULL
    #ifdef __cplusplus			//判断代码是不是c++
    #define  NULL         0		//如果是c++,就将NULL 宏定义为0,这是int类型
    #else
    #define  NULL	((void  *)0)		//否则被定义为无类型指针(void*)的常量
    #endif
    #endif
    

所以如果初始化指针变量 = NULL是错误的。

在c++11中,为了考虑兼容性,C++11并没有消除常量0的二义性,C++11给出了全新的nullptr表示空值指针。C++11为什么 不在NULL的基础上进行扩展,这是因为NULL以前就是一个宏,而且不同的编译器厂商对于NULL的实现可能 不太相同,而且直接扩展NULL,可能会影响以前旧的程序。

因此:为了避免混淆,C++11提供了nullptr, 即:nullptr代表一个指针空值常量。nullptr是有类型的,其类型为nullptr_t,仅仅可以被隐式转化为指针类 型,nullptr_t被定义在头文件中。


注意:

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

你可能感兴趣的:(c++)