C++入门

C++入门


序言:适合C语言有基础,从C语言过度进入C++的学习

文章目录

  1. 命名空间

  2. C++的输入与输出

  3. 缺省参数

  4. 函数重载

  5. 引用

  6. 内联函数

  7. auto关键字

  8. 基于范围的for循环

  9. 指针空值nullptr


1.命名空间

在C/C++中,变量,函数和类都是大量存在的,这些变量,函数和类的名称都将存在全局作用域中,会导致冲突

使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染namespace关键字的出现就是解决这种问题的

#include<stdio.h>

int scanf = 0;//全局变量scanf
//这里将整形变量的名称定义成scanf,跟库函数中的scanf的命名冲突
int main()
{
    printf("%d",scanf)
    return 0;
}
//编译器报错:"scanf":重定义;以前定义是"函数"
//C语言无法解决这样的问题,C++引入命名空间

1.1定义

定义命名空间,需要使用namespace关键字,后面跟命名空间的名字,再接**{命名空间成员}**

//1.正常使用(命名空间只能全局定义)
namespace ddy//名字随意起,笔者的名字是东东羊
{
    //命名空间可以定义变量/函数/类型
    int scanf = 0;//变量
    
    int Add(int x, int y)//函数
    {
        return x+y;
    }
    
    struct Node//类型(C语言中是结构体,C++引入了类的概念)
    {
        struct Node* next;
        int val;
	};
}

//2.命名空间可以嵌套使用
namespace N1
{
    int a=0;
    namespace N2
    {
        int a=1;//这里的a是N2的a,与N1中的a不冲突
	}
}

//3.同一个工程中运行存在多个相同名称的命名空间,编译器最终会合成同一个命名空间
//test.h
namespace M1
{
    int a;
    int b;
    int Add(int x, int y)
    {
        return x+y;
    }
}

//test.cpp
namespace M1
{
    int Sub(int x, int y)
    {
        return x-y;
	}
}

namespace M2
{
    int c;
    int d;
}

//即编译器编译时,会把头文件的M1与源文件的M1合并在一起,形成的新的M1包含a,b,c,d,Add,Sub

注意: 一个命名空间就定义了一个作用域,命名空间的所有内容都局限于该命名空间中

1.2使用

namespace ddy
{
    int a=10;
    int b=5;
}

命名空间的使用有种方式:

  • 加命名空间名称及作用域限定符(:😃
int main()
{
    printf("%d\n",ddy::a);
    return 0;
}
  • 使用using将命名空间中的某位成员引入
using ddy::b;
int main()
{
    printf("%d\n",ddy::a);
    printf("%d\n",b);//此处的b不需要再加作用域限定符
    return 0;
}
  • 使用 using namespace 命名空间名称 引入
using namespace ddy;
int main()
{
    printf("%d\n",a);
    printf("%d\n",b);//此处的a,b不再需要加作用域限定符
    return 0;
}

2.C++的输入与输出

#include<iostream>	
using namespace std;
//std是C++标准库的命名空间名,C++将标准库的定义实现都放到了这个命名空间中

int main()
{
    cout<<"hello world!"<<endl;
    return 0;
}

说明:

  1. 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含头文件以及按命名空间使用方法使用std

  2. coutcin是全局的流对象,endl是特殊的C++符号,表示换行输出,它们都包含在头文件中

  3. coutcin可以自动识别变量类型,不需要像C语言中的printf/scanf那样手动控制变量类型

  4. ***<<是流插入运算符,>>***是流提取运算符

#include<iostream>	
using namespace std;

int main()
{
    char a;
    int b;
    double c;
    
    //cin自动识别类型
    cin>>a;
    cin>>b>>c;
    
    //cout自动识别类型
    cout<<a<<endl;
    cout<<b<<" "<<c<<endl;
    return 0;
}

3.缺省参数

3.1概念

缺省参数是声明或定义函数时为函数的参数指定一个缺省值

在调用函数时,没有指定的实参则用该函数的缺省值代替,有则使用指定的实参

void Func(int a = 10)
{
    cout<<a<<endl;
}

int main()
{
    Func();		//没有传参,就用参数的默认值
    Func(1);		//传参时,使用指定的实参
    return 0;
}

3.2分类

  • 全缺省参数
void Func(int a=1, int b=2, int c=3)	//每个参数都有缺省值
{
    cout<<"a = "<<a<<endl;
    cout<<"b = "<<b<<endl;
    cout<<"c = "<<c<<endl;
}
  • 半缺省参数
void Func(int a, int b=2, int c=3)	//缺省参数必须从右往左给,中间不能隔着给
{
    cout<<"a = "<<a<<endl;
    cout<<"b = "<<b<<endl;
    cout<<"c = "<<c<<endl;
}

int main()
{
    Func(1);	//传参,此时a=1,b=2,c=3
    Func(1,4);	//传参,此时a=1,b=4,c=3
    return 0;
}

说明:

  1. 缺省参数不能在函数声明和定义中重复出现
//test.h
void Func(int a = 10)	//声明
    
//test.cpp
void Func(int a = 20)	//定义
{}
   
//如果声明和定义同时出现,并且两个位置提供的缺省值不同,则编译器就无法确定该用哪一个缺省值

//解决
//test.h
void Func(int a = 10)	//推荐将缺省参数写到声明中,便于在头文件里查找修改
    
//test.cpp
void Func(int a)	//定义不写缺省参数
  1. 缺省值必须是常量或者全局变量
  2. C语言不支持(编译器不支持)

4.函数重载

4.1概念

函数重载:是函数的一种特殊情况,C++运行在同一作用域中声明几个功能相似的同名函数,这些同名的函数的形参(类型/个数/顺序)不同,常用来处理实现功能类似数据类型不同的问题

#include<iostream>
using namespace std;

//1.参数不同
int Add(int x, int y)
{
    cout<<"int Add(int x, int y)"<<endl;
    return x+y;
}

double Add(double x, double y)
{
    cout<<"double Add(double x, double y)"<<endl;
    return x+y;
}

//2.个数不同
void f()
{
    cout<<"f()"<<endl;
}

void f(int a)
{
   	cout<<"f(int a)"<<endl;	//这里输出的就是f(int a),跟a的值没有关系
}

//3.顺序不同
void f(int a, char b)
{
    cout<<a<<endl;
    cout<<b<<endl;
}

void f(char a, int b)
{
    cout<<a<<endl;
    cout<<b<<endl;
}

int main()
{
    Add(1,2);
    Add(1.1,2.1);
    
    f();
    f(1);
    
    f(1,'a');
    f('a',1);
    return 0;
}

4.2C++支持函数重载的原理–名字修饰

环境:在Linux下,采用**g++**编译

  1. 在C/C++中,一个程序想要运行起来,需要经过以下几个阶段:预处理、编译、汇编、链接,这四个过程合在一起,我们也统称为编译

    list.h list.c test.c

    预处理:头文件展开/宏替换/条件编译/去掉注释

    list.i test.i //此时的list.h已被展开在两个.c文件里

    编译:检查语法,生成汇编语言

    list.s test.s

    汇编:汇编代码转成二进制的机器码

    list.o test.o

    链接:将所有的目标文件链接到一起(合并段表、符号表的合并和符号表的重定位) //C语言不支持函数重载而C++支持的区别之处

    a.out

  2. 链接是C++支持函数重载的关键,用gcc编译C语言时,函数修饰后在符号表名字不变,而g++编译C++时,函数修饰后在符号表名字发生改变

    //gcc
    int Add(int a, int b)		//<Add>	即相同命名的函数,在链接时符号表的名字都是<Add>,此时编译器无法做出选择
    {}
    
    //g++
    int Add(int a, int b)		//<_Z2Addii>
    {}
    
    int Add(char a)				//<_Z1Addc>
    {}
    							//g++的命名规则是:_Z + 参数数量 + 函数名称 + 参数类型首字母
    							//在此规则下,相同命名的函数,只要参数不同,编译都可通过,所以C++支持函数重载
    
  3. 通过命名规则,发现函数重载只与参数有关,返回值不同是不构成重载的,因为编译器没有办法区分

5.引用

5.1概念

引用不是定义一个新变量,而是给已经存在的变量取新名字,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块空间

使用方法:类型& 引用变量名 = 引用实体

int main()
{
    int a = 10;
    int& b = a;	//定义引用类型
}

注意引用类型必须和引用实体是同种类型

5.2特性

  1. 引用在定义时必须初始化

    int main()
    {
        int& a;	//error
        
        int a = 10;
        int& b = a;	//right
        return 0;
    }
    
  2. 一个变量可以有多个引用

    int main()
    {
        int a = 10;
        int& b = a;	
        int& c = b;	//a、b、c现在是同一块空间,即:原本叫a的内存空间,现在又可以叫b、c
    }
    
  3. 引用一旦引用了一个实体,再不能引用其他实体

    int main()
    {
        int a = 10;
        int b = 5;
        int& c = a;	
        c = b;	//是c又再引用b吗?还是把b赋值给了c
        		//这里是把b赋值给了c,c已经引用了a,就不能再引用其他实体了
    } 
    

5.3常引用

int main()
{
    const int a = 10;	//加const修饰,a的权限变成只能读不能写
    int& b = a;	//error		原因:用未被const修饰的b来引用a,此时的权限相对于放大,变成可读可写,不允许
    const int& c = a; //right
    
    int d = 5;
    const int e = d;//?		right  原因:d的权限是可读可写,加const修饰的e引用后,e的权限相对d缩小,允许
    return 0;
}

总结

  1. 权限的大小变更只存在于引用指针
  2. 权限可缩小,不可放大

5.4使用场景

  1. 参数

    //函数调用的三种方式:传值调用、传址调用、引用调用(C++)
    void Swap(int a, int b)		//传值调用,无法实现交换实参的目的
    {
        int tmp = a;
        a = b;
        b = tmp;
    }	
    
    void Swap(int& a, int& b)	//引用调用,可以实现交换实参的目的
    {
        int tmp = a;			//思考:传值调用和引用调用的这两个函数是否可以构成函数重载?
        a = b;					//答:不构成,引用类型和引用实体是同一类型,即:int& a 的类型是int,
        b = tmp;				//那么意味着两个Swap的参数是一样的,不构成函数重载
    }
    
    void Swap(int *pa, int* pb)	//传址调用,可以实现交换实参的目的
    {
        int tmp = *pa;
        *pa = *pb;
        *pb = tmp;
    }
    
  2. 返回值

    //正常的返回值
    int Add(int a, int b)
    {
        int ret = a+b;
        return ret;		//ret返回的是一个临时空间,此空间开辟在栈区
    }
    
    int main()
    {
        int n = Add(1,2);	//n在接受ret返回的值后,ret就会被销毁
        return 0;			//但是需要注意的是,ret原先的空间还在那个位置,只是不再属于ret,即权限系统收回
    }
    
    //引用做返回值
    int& Add(int a, int b)
    {
        int ret = a+b;
        return ret;
    }
    
    int main()
    {
        int& a = Add(1,2);
        Add(3,4);
        cout<<a<<endl;	//7	为什么a是7呢?
        return 0;		//答:ret开在栈区,a引用ret,Add(1,2)结束后,ret被销毁,空间也被系统收回,此时a依然在
    }					//引用ret已经被收回的空间,再继续执行Add(3,4),碰巧此时的空间又被赋给新的ret,新的ret等
    					//于7,则a也为7
    
    //思考:引用做返回值,如果返回值的生命周期短,出了函数就被销毁了,那么引用的返回值就不安全,不妨延长返回值的生命周期
    
    //-------------------------------------------------------------------------------------------------------
    int& Add(int a, int b)
    {
        static int ret = a+b;	//static修饰变量后,ret这个变量将会被开辟在静态区中,ret不会再被销毁,
        return ret;				//即:延长ret的生命周期,直到程序结束
    }
    
    int main()
    {
        int& a = Add(1,2);
        int& b = Add(3,4);
        
        cout<<a<<endl;	//3
        cout<<b<<endl;	//3 为什么b也是3呢?
        return 0;		//答:ret开辟在静态区,Add运行第一次的时候开辟,第二次就不会再开辟,用的还是第一次开辟的ret
    }
    
    //思考:想要保证引用返回值的安全,就要将其的生命周期延长,但延长后,后续的返回值都是第一次执行时的返回值,相当于函数只能
    //执行一次,从当前所学的知识来看很鸡肋,后续再回头看
    

总结:一个函数想要使用引用作为返回参数,就需要返回参数出了函数的作用域还存在(全局变量/静态变量),否则就不安全

5.5传值、传引用效率比较

结论:传值调用效率低

原因:以值作为参数或者返回值类型时,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其当参数或者返回值类型非常大(例如:结构体)时,效率更低

5.6引用和指针的区别

  1. 语法概念上,引用就是一个别名,不开辟新的空间,与其引用实体共用同一块空间

    底层实现上,引用是有空间的,因为引用是按照指针方式实现的

  2. 概念上,引用定义了一个变量的别名,指针储存一个变量地址

  3. 引用定义时必须初始化,指针没有要求(但还是建议将指针置成空指针)

  4. 引用在初始化后,不能再引用其他实体,指针在任何时候都可以指向任何一个同类型实体

  5. 没有NULL引用,但有NULL指针

  6. 在sizeof中的含义不同引用的结果是引用类型的大小,但指针始终是地址空间所占字节个数(32位平台占4字节,64位平台占8字节)

  7. 引用自加,所引用的实体加1,指针自加,指针向后偏移一个类型的大小

  8. 有多级指针,没有多级引用

  9. 访问实体方式不同,指针需要解引用,引用编译器自己处理

  10. 引用指针使用起来相对安全

6.内联函数

6.1概念

inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,不再产生函数调用建立堆栈的开销,内联函数提升程序运行的效率

inline int Add(int a, int b)
{
    return a+b;
}

int main()
{
    int a = Add(1,2);	//此时,Add函数被inline修饰成为内联函数,不再调用函数建立栈帧,而是直接展开
    return 0;
}

//编译时
int main()
{
    int a = 1+2;	//Add直接展开,类似于C语言中的宏函数
    return 0;
}

6.2特性

  1. inline是一种空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段会用函数体代替函数调用

    缺陷:可能会使目标文件变大

    优点:少了函数调用开销,提高了程序调用效率

  2. 内联函数对于编译器而言只是一个建议,不同的编译器关于inline实现的机制不同

    一般建议:将函数规模较小、不是递归、且频繁调用的函数采用inline修饰,否则编译器可能会忽略inline特性

  3. inline不建议声明和定义分离,分离会导致链接错误,因为inline被展开后,就没有函数地址了,链接器就会找不到

    但是由于现代编译器允许链接时优化,所以即使把inline写在.cpp,也有概率被inline

面试题

  1. 宏的优缺点?

    优点:

    1. 提高代码的复用性
    2. 提高性能

    缺点:

    1. 不方便调试(因为预处理阶段对宏进行了替换)
    2. 导致代码可读性差,可维护性差,容易误用
    3. 没有类型安全的检查
  2. C++有哪些技术可以代替宏?

    //1.常量定义换成const enum
    #define N 10 
    //换成
    const int N = 10;
    
    //2.短小函数(20行以内)定义换用内联函数
    #dedine Add(x,y) ((x)+(y))//宏函数
    //换成
    inline Add(int x, int y)
    {
        return x+y;
    }
    

7.auto关键字

环境:VS2022 64位平台

### 7.1简介

作用:在C++11中,auto是用来自动推导表达式变量的实际类型的

#include<iostream>
using namespace std;

int main()
{
    int a = 0;
    auto b = a;//b此时为int类型
    auto c;	//error  使用auto定义变量时,必须初始化
    
    cout<<typeid(b).name()<<endl;	//typeid().name()可以自动识别变量类型
    return 0;
}

注意

使用auto定义变量时,必须对其初始化,因为在编译阶段编译器需要根据初始化表达式来推导auto的实际类型

因此auto并非是一种"类型"的声明,而是一个类型声明时的"占位符",编译器在编译阶段将auto替换成变量的实际类型

7.2使用细则

  1. auto引用指针结合起来使用

    //用auto声明指针类型时,用auto和auto*没有区别
    //但auto声明引用时必须加&,即auto&
    int main()
    {
        int a = 10;
        
        auto pa1 = &a;
        auto* pa2 = &a;
        auto& b = a;	//声明必须加&
        
        cout<<typeid(pa1).name()<<endl;	//int * __ptr64
        cout<<typeid(pa2).name()<<endl;	//int * __ptr64
        cout<<typeid(b).name()<<endl;	//int
        
        return 0;
    }
    
  2. 在同一行定义多个变量

    //当在同一行定义多个变量时,这些变量必须是相同的类型,否则编译器会报错
    //因为编译器只对第一个类型进行推导,然后用推导出来的类型来定义后面的其他变量
    int main()
    {
        auto a = 1, b = 2;//right a和b都是整形,用int定义
        auto c = 3, d = 4.0;//error c是整形,则auto推导出int,将c连同d一起定义成int,但d是浮点型,编译器编译失败	
    }
    

7.3注意事项

  1. auto不能作为函数的参数

    void TestAuto(auto a)	//error,auto不能作为形参的类型,因为编译器无法对a的实际类型进行推导
    {}
    
  2. auto不能直接定义数组

    void TestAuto()
    {
        int a = {1,2,3};
        auto b = {4,5,6};//error
    }
    
  3. 为了避免与C++98的auto发生混淆,C++11仅保留了auto作为类型指示符的用法

  4. auto在实际中最常见的优势用法就是跟下一节讲到的C++提供的新式for循环,还有lambda表达式等进行配合使用

8.基于范围的for循环

8.1语法

C++98中要遍历数组,可以按照一下方式进行

void TestFor()
{
    int a = {1,2,3,4,5};
    for(int i = 0; i<sizeof(a)/sizeof(a[0]);i++)
        a[i]*=2;
}

对于一个有范围的集合而言,由程序员说明循环的范围是多余的,有时候还会犯错

因此在C++11中引入了基于范围的for循环

for(declaration : expression)

declaration表示遍历声明,在遍历过程中,当前被遍历到的元素会被储存在声明的变量中

expression是要遍历的对象,可以是表达式、容器、数组、初始化列表

void TestFor()
{
    int a = {1,2,3,4,5};
    for(auto e:a)
        e*=2;	//运行发现,这样的代码是达不到让a数组中的每个数都乘2的目的的,为什么?
				//答:变量e其实相当于a数组里元素的临时拷贝,即改变e也不会a数组,那么我们就可以采用引用
    for(auto& e:a)
        e*=2;	//这样a数组中的元素都乘2了
				
}				

注意:范围for与普通循环类似,可以用continue来结束本次循环,也可以用break跳出整个循环

8.2使用条件

  1. for循环的范围必须是确定的

    对于数组而言,就是数组中的第一个元素和最后一个元素的范围

    void TestFor(int arr[])	//error 当数组作为函数参数时,数组名退化为指针,不能使用范围for
    {
        for(auto e:arr)
            cout<<e<<endl;
    }
    
  2. 迭代的对象要实现++和==的操作

9.指针空值nullptr(C++11)

C++98中的指针空值

在良好的C/C++编程习惯中,定义变量最好都初始化,否则可能会产生不可预料的错误

比如一个未初始化的指针,如果一个指针没有初始化,那么我们以往都是按照下面的方式进行初始化:

void TestPtr()
{
    int* p1 = NULL;
    int* p2 = 0;
}

NULL实际上是一个,在传统的C头文件(stddef.h)中,可以看到如下代码:

#ifndef NULL
#ifdef __cplusplus
#define NULL   0
#else
#define NULL   ((void *)0)
#endif
#endif

可以看到,NULL可能被定义成字面的0,或者被定义成无类型指针(void*)的常量

void f(int a)
{
    cout<<"f(int)"<<endl;
}

void f(int* p)
{
    cout<<"f(int*)"<<endl;
}

int main()
{
    f(0);	//f(int)
    f(NULL);//f(int)	思考:NULL是指针啊,为什么会被判断为整数?
    					//答:因为NULL被定义成0,因此调用不了f(int* p)
    f((int*)NULL);	//f(int*)
}

在C++98中,字面常量0既可以是一个整数数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成一个整形常量

如果要将其按照指针的方式使用,必须对其进行强转**(void*)0**

注意

  1. nullptr在C++11中,是作为新的关键字引入的,不需要再引头文件
  2. 在C++11中,sizeof(nullptr)sizeof((void*)0)算出来的字节大小是相等的(64位平台是8字节,32位平台是4字节)
  3. 为了提高代码的健壮性,建议后续指针置空都用nullptr
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值