【C++】1.C++基础

1.命名空间

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

1°定义

定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。

  • 普通的命名空间
namespace N1
{
  int a;
  int Add(int left, int right)
  {
    return left + right;
  }
}
  • 命名空间可以嵌套
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;
    }
  }
}
  • 同一个工程中允许存在多个相同名称的命名空间

    编译器最后会合成同一个命名空间中(可以写两个N1)

namespace N1
{
  int Mul(int left, int right)
  {
    return left * right;
  }
}

2°使用

  • 加命名空间名称及作用域限定符 ps: N::a
  • 使用using将命名空间中成员引入 ps: using N::b
  • 使用using namespace 命名空间名称引入 ps: using namespace N;

输入输出 cin和cout都在std这个命名空间内

#include <iostream>
using namespace std;

int main()
{
  //1.std::不用using namespace std    endl换行
  std::cout << "hello world"<<std::endl;
  int i = 1;
  double d = 1.11;
  cin >> i >> d;
  //2.加了using namespace std  不需要std::
  cout << i << " " << d << endl;
  return 0;
}

2.缺省参数

1°概念

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

例如:

#include <iostream>
using namespace std;

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

2°分类

  • 全缺省参数
  • 半缺省参数
#include <iostream>
using namespace std;

//全缺省 所有参数都给
//可以传1个 也可以传多个
void Func1(int a = 10, int b = 20, int c = 30)
{
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl;
}

//半缺省 给部分参数
//必须传1个(没有缺省的位置必须传) 也可以多个
//必须从右往左连续缺省 右边的必须先给值
void Func2(int a, int b = 10, int c = 20)
{
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl;
}

int main()
{
    Func1();//10 20 30
    Func1(1);//1 20 30
    Func1(1, 2);//1 2 30
    Func1(1, 2, 3);//1 2 3
    Func2(1);//1 10 20
    Func2(1, 2);//1 2 20
    Func2(1, 2, 3);//1 2 3
}

3°注意

  • 半缺省参数必须从右往左连续给出 不可以间隔
  • 缺省参数不可以在函数的声明和定义中同时出现 编译器无法确定给哪个缺省值
  • 缺省值必须是常量或者全局变量
  • C语言不支持(编译器不支持)

3.函数重载

1°概念

  • 一个函数可以有多个意义
  • 要求:函数名相同 参数不同(类型/个数/顺序不同)
  • 不要参数也可以 返回值相同和不同都可以

注意:只有返回类型不同不能构成重载 必须有参数不同

void Func()

int Func()

short Add(short left, short right)

{

        return left+right;

}

int Add(short left, short right)

{

        return left+right;

}

都不构成函数重载

2°名字修饰

为什么C语言不支持函数重载 而C++支持呢?

在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。

编译链接过程

  1. 预处理 -> 头文件的展开/宏替换/条件编译/去掉注释 list.h list.c test.c
  2. 编译 -> 检查语法生成汇编代码 list.i test.i
  3. 汇编 -> 汇编代码转成二进制的机器码 list.s test.s
  4. 链接 -> 将两个目标文件链接到一起 list.o test.o

编译这一步时

  • C:在linux下,采用gcc编译完成后,函数名字的修饰没有发生改变
  • C++:在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。

原因

  • C语言没办法支持重载,因为同名函数没办法区分。(编译器在编译.c文件时,只会给函数进行简单的重命名)
  • C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。

3°extern "C"

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

4°练习

  • 下面两个函数能形成函数重载吗?有问题吗或者什么情况下会出问题?
void TestFunc(int a = 10)
{
  cout<<"void TestFunc(int)"<<endl;
}
void TestFunc(int a)
{
  cout<<"void TestFunc(int)"<<endl;
}

不形成函数重载 因为两个函数的参数相同

  • C语言中为什么不能支持函数重载?

C语言没办法支持重载,因为同名函数没办法区分。(编译器在编译.c文件时,只会给函数进行简单的重命名)

C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。

  • C++中函数重载底层是怎么处理的?

在.cpp文件中,虽然两个函数的函数名一样,但是他们在符号表中生成的名称不一样。

  • C++中能否将一个函数按照C的风格来编译?

可以 在函数前加extern "C"

4.引用

1°概念

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

#include <iostream>
using namespace std;

int main()
{
    int a = 1;
    int& ra = a;//ra是a的引用 引用也就是别名 a再取了一个名称ra
    int& b = a;
    int& c = b;
    //4个名字公用一块空间
    //改变任何一个别名的值 每个别名和变量本身都会改变
    c = 10;
    //10 10 10 10
    cout << a << " " << ra << " " << b << " " << c << " " << endl;
}

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

2°引用特性

  • 引用在定义时必须初始化
  • 一个变量可以有多个引用
  • 引用一旦引用一个实体,再不能引用其他实体
#include <iostream>
using namespace std;

int main()
{
    int a = 1;
    //int& b; //引用必须在定义时初始化
    int& c = a;
    int d = 2;
    c = d; 
    //分析:这里是c变成了d的引用? no 
    //还是d赋值给c? ok
    //说明c已经是a的引用了 不会再是其他变量的引用
    //结论:引用一旦引用一个实体,再不能引用其他实体
    return 0;
}

3°常引用

#include <iostream>
using namespace std;

int main()
{
    //int a = 0;
    //int& b = a;//b的类型是int
    //如果
    //const int a = 0;
    //int& b = a;//b的类型是int 编译不通过 原因:a是只读 b的类型是int 可读 可写 所以不行
    const int a = 0;
    const int& b = a;
    int c = 1;
    int& d = c;
    const int& e = c;
    //行不行?可以的-> C可读可写 e变成别名只读
    //总结:引用取别名时 变量访问的权限可以缩小 不能放大 
    //取别名变量范围小
    return 0;
}

别名是可读可写 如果变量只可读 那么取别名编译失败

别名的权限较小

进阶:隐式类型转换+取别名

#include <iostream>
using namespace std;

int main()
{
    int i = 0;
    double d = i;//隐式类型转换
    //double& rb = i;
    //float& rf = i;
    //跟字节大小无关
    //无法通过编译
    //但加const 可以
    const double& rd = i;
    const float& rf = i;
    //为什么?
    //i->double临时变量->db/rd/rf
    //临时变量具有常性(const只可读)
    //所以不加const就放大了权限 加const就可以成立
    //变量之间赋值没有权限缩小和放大的关系 引用才有
    const int ci = i;
    int x = ci;
    return 0;
}
  • 引用:

一个int变量给到一个double变量

int变量->double临时变量(临时变量具有常性)->取别名的时候只能是const类型的

权限不能放大

  • 赋值:

都可以 没有权限问题

4.使用场景

1°作参数

void swap_c(int* p1, int* p2)
{
    int tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
}
//r1是a的别名 r2是b的别名 r1和r2的交换就是a和b的交换
void swap_cpp(int& r1, int& r2)
{
    int tmp = r1;
    r1 = r2;
    r2 = tmp;
}

2°作返回值

#include <iostream>
using namespace std;

//返回一个临时变量(tmp) tmp的类型是int tmp的值是n的值
//这里会多产生一个tmp的空间
int Count1()
{
    static int n = 0;
    n++;
    return n;
}

//这个时候tmp就是n的别名 返回的就是n
//这里不会产生多的空间
int& Count2()
{
    static int n = 0;
    n++;
    return n;
}

int main()
{
    //int& r1 = Count1();编译不通过 返回的是tmp(临时变量) 临时变量具有常性 只可读 给到可读可写 不行
    const int& r1 = Count1();
    int& r2 = Count2();//返回的就是tmp(n的别名) 不是临时变量
    return 0;
}
  • 传值返回和隐式类型转换都先要产生一个临时变量(拷贝) **临时变量具有常性 **
  • 此时传值返回直接取别名可能有权限放大问题
  • 此时隐式类型转换取别名不加const会有问题

但引用返回就规避了问题 因为引用和原本变量是同一空间 可以理解就是返回原来的变量

注意:

int& Add(int a, int b)
{
    int c = a + b;
    return c;
    //局部变量(栈上) 返回后 栈销毁了
    //返回c的别名
}

int main()
{
    //返回之后用了c的别名 再取一个别名ret
    //栈已经销毁还给系统 还去使用这块空间 会出现随机值
    int& ret = Add(1, 2);
    cout << "Add(1, 2) is :" << ret << endl;
    return 0;
}

此时c是引用返回 但c是局部变量(栈上) 出了Add函数作用域后 栈销毁了 还给了系统

此时还去使用这块空间 出现随机值

解决方法:

  • 如果还想要引用返回 那就加上static(static int c) 这样c的空间不会出函数作用域就销毁
  • 使用传值返回

注意当使用static时(那条语句只会被执行一次):

int& Add(int a, int b)
{
    static int c = a + b;
    //这条语句只会被执行一次(第二次及以后调用函数不会再次执行)
    //只会被定义一次变量
    return c;
}
int main()
{
    int& ret1 = Add(1, 2);
    int& ret2 = Add(3, 4);//就算再调用一次 c还是3
    cout << "Add(1, 2) is :" << ret1 << endl;//3
    cout << "Add(3, 4) is :" << ret2 << endl;//3
    return 0;
}

如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回

什么变量可以使用引用返回:全局变量 静态变量

3°传值vs传引用

  • 传值:会产生一份临时拷贝并返回
  • 传引用:直接返回本来变量
  • 传值效率更低(当参数或者返回值类型非常大)

那么函数使用引用返回好处是什么?->少开辟一个拷贝变量的空间 提高程序的效率

作参数时

#include <iostream>
#include <time.h>
using namespace std;

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(A)-time:" << end1 - begin1 << endl;
    cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
    TestRefAndValue();
    return 0;
}

作为返回类型

#include <iostream>
#include <time.h>
using namespace std;

struct A 
{ 
    int a[10000]; 
};
//40000byte

//值返回
A TestFunc1(A a) 
{
    return a;
}

//引用返回
A& TestFunc2(A& a) 
{
    return 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(A)  time:" << end1 - begin1 << endl;
    cout << "TestFunc2(A&) time:" << end2 - begin2 << endl;
}

int main()
{
    TestRefAndValue();
    return 0;
}

4°引用vs指针

初始化 用法 sizeof +- 安全等方面

不同:

  1. 引用在定义时必须初始化,指针没有要求
  2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  3. 没有NULL引用,但有NULL指针
  4. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
  5. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  6. 多级指针,但是没有多级引用
  7. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  8. 引用比指针使用起来相对更安全

5.内联函数

1°概念

以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开没有函数压栈的开销,内联函数提升程序运行的效率。

2°特性

  • inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数
  • inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。
  • inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
  • 一般内联适用于小函数 小于20行
#include <iostream>
using namespace std;

//在前面加inline(内联) 空间换时间
//原:Call Swap(1w)+3行指令
//现:Call Swap(1w*3) 直接全部展开了 空间变大了 但时间减少了
inline void Swap(int& x1, int& x2)
{
    int tmp = x1;
    x1 = x2;
    x2 = tmp;
}

//频繁调用Swap 调用函数需要建立栈帧 是有消耗的
//如何解决:1.C语言使用宏函数 2.C++使用内联函数

int main()
{
    int a = 0;
    int b = 2;
    Swap(a, b);
    return 0;
}

3°练习

宏的优缺点?

优点:

  • 增强代码的复用性。
  • 提高性能。

缺点:

  • 不方便调试宏。(因为预编译阶段进行了替换)
  • 导致代码可读性差,可维护性差,容易误用。
  • 没有类型安全的检查 。

C++有哪些技术替代宏?

  • 常量定义 换用**const ** #define N 10; -> const int N = 10;
  • 函数定义 换用内联函数

6.auto关键字(C++11)

1°简介

C++11:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型

int a = 0;

auto b = a;

此时b的类型就会根据a的类型推导出来

2°使用

  • auto与指针和引用结合起来使用

用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&

#include <iostream>
using namespace std;

int main()
{
    int a = 0;
    auto b = a;//b的类型是根据a的类型推导出是int
    int& c = a;

    auto& d = a;//引用类型加上&
    auto* e = &a;
    auto f = &a;//不加*也可以推出来指针 指针可以不加

    cout << typeid(a).name() << endl;
    cout << typeid(b).name() << endl;
    cout << typeid(c).name() << endl;
    
    cout << typeid(d).name() << endl;
    cout << typeid(e).name() << endl;
    cout << typeid(f).name() << endl;

    //auto实际作用
    //类型很长可以直接写成auto 
    //使用auto来进行优化 简化代码的方法
    return 0;
}
  • 在同一行定义多个变量

当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

auto a=1,b=2;

auto c=3,d=4.0;

第二行编译失败 因为auto已经是int类型了 auto d = 4.0不可以

3°auto不能推导的情况

参数和数组不能用auto

auto不能作为形参类型

auto不能直接用来声明函数

7.范围for循环(C++11)

C++98:for(int i = 0;i<sizeof(arr)/sizeof(arr[0]);++i)

C++11:for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

#include <iostream>
using namespace std;

int main()
{
    int array[] = { 1,2,3,4,5 };
    //要求将数据中的值乘2倍 再打印一遍

    //C语言的做法
    for (int i = 0; i < sizeof(array) / sizeof(int); ++i)
    {
        array[i] *= 2;
    }
    for (int i = 0; i < sizeof(array) / sizeof(int); ++i)
    {
        cout << array[i] << " ";
    }
    cout << endl;

    //C++11->范围for(语法糖)->特点:写起来比较简单
    //e是别名 e的改变就是数组的改变
    //可以看成array中每个元素取出来赋值给e
    //e的改变不会影响array中元素的改变
    //e如果是别名就可以
    for (auto& e : array)
    {
        e *= 2;
    }
    for (auto e : array)
    {
        cout << e << " ";
    }
    cout << endl;
    return 0;
}

注意: for循环迭代的范围必须是确定的

void TestFor(int array[]) { for (auto& e : array)//这里传参 array是首元素地址 无法通过编译 cout << e << endl; }

这里是首元素地址 for循环范围不确定

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

C++98: int* p = NULL;

NULL实际是一个宏

stddef.h中:

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

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

字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量。

但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。

这里就会遇到麻烦 你想的是指针 但编译器会默认为整形常量

所以C++11引用nullptr关键字

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

【C++】1.C++基础 完

猜你喜欢

转载自blog.csdn.net/szh0331/article/details/129108789
今日推荐