C++入门

148 阅读11分钟

C++基础(大约150h): C++语法;高阶数据结构;STL

1. C++入门

1.1 作用域namespace

1.11 作用域/局部域

//作用域/局部域
#include<iostream>
#include<cstdio>
​
using namespace std;
​
int a = 2;
​
void foo()
{
   int a = 0;
   cout << a << endl;
   cout << ::a << endl//想要打印全局变量的a,需要加域作用限定符::
}
int main()
{
   cout << a << endl;
   foo();
   return 0;
}

1.12 使用作用域解决两个头文件有相同变量名的方式

//使用作用域解决两个头文件有相同变量名的方式
//list1.h
#pragma once
#include<cstdio>
#include<iostream>namespace Alist
{
   struct Node //Alist里的Node是个双链表
  {
       struct Node* prev;
       struct Node* next;
       int val;
  };
}
​
//list2.h
#pragma once
#include<cstdio>
#include<iostream>
​
​
namespace Blist
{
   struct Node //Blist里的Node是个双链表
  {
       struct Node* next;
       int val;
  };
}
​
//test.cpp
#include "list1.h"
#include "list2.h"int main()
{
   struct Alist::Node n1;
   struct Blist::Node n2;
   n1.val = 1;
   n2.val = 2;
   std::cout << n1.val << " " << n2.val <<std::endl;
   return 0;
}

1.13 命名空间可以嵌套

//list1.h
#pragma once
#include<cstdio>
#include<iostream>namespace Alist
{
   namespace A{
       struct Node //Alist里的Node是个双链表
      {
           struct Node* prev;
           struct Node* next;
           int val;
      };
  }
​
   namespace B
  {
       struct Node //Blist里的Node是个双链表
      {
           struct Node* next;
           int val;
      };
  }
}
​
//test.cpp
#include "list1.h"int main()
{
   struct Alist::A::Node n1;
   struct Alist::B::Node n2;
   
   return 0;
}

1.14 部分展开

实际项目中:指定命令空间展开;常用部分展开

#include "list1.h"
​
using std::cout;
using std::cout;
using std::endl;
int main()
{
   cout << "hello world" << endl;
   
   return 0;
}

1.2 缺省

一般在声明里给缺省

#include <iostream>
#include <cstdio>
​
using namespace std;
​
void func(int a = 10)//缺省
{
   cout << a << endl;
}
​
//全缺省
void func2(int a = 10,int b = 20,int  c = 30)
{
   cout << a << endl;
   cout << b << endl;
   cout << c << endl;
}
​
//半缺省,遵循从右往左
void func3(int a , int b = 1int c = 2)
{
   cout << a << endl;
   cout << b << endl;
   cout << c << endl;
}
int main()
{
   func();
   func(20);
​
   func2();
   func2(1,2);
   func2(1);
​
   func3(30);
   return 0;
}

1.3 函数重载

用来研究名字修饰规则,借助以下指令

查看汇编代码
objdump -d <file>
​
查看符号表
objdump -t <file>
​
查看段信息
objdump -h <file>
​
既提供源代码又可以查看汇编代码
objdump -S <file>

C++ 中的函数重载允许你定义具有相同名称但参数列表不同的多个函数。函数重载提供了更灵活的方式来处理不同类型的参数或不同数量的参数

在C++中,函数重载必须满足以下规范:

  1. 相同函数名: 重载的函数必须有相同的函数名。
  2. 不同参数列表: 重载的函数必须有不同的参数列表,包括参数的类型、顺序或数量。
  3. 不同返回类型: 仅仅通过返回类型的不同是不足以实现函数重载的。参数列表必须不同。
  4. 相同作用域: 重载的函数必须在相同的作用域内,即在同一个类或命名空间内。
  5. 函数特征标(Function Signature)的唯一性: 重载的函数必须具有不同的函数特征标,即参数列表的类型、顺序或数量必须有所不同。

函数特征标是由参数列表、参数类型和返回类型组成的签名。以下是一些例子:

cppCopy code
void foo(int x);         // 特征标:foo(int)
void foo(double x);      // 特征标:foo(double)
void bar(int x, double y)// 特征标:bar(int, double)
void bar(double x, int y)// 特征标:bar(double, int)

C++ 中的函数重载允许你定义具有相同名称但参数列表不同的多个函数。函数重载提供了更灵活的方式来处理不同类型的参数或不同数量的参数。以下是一些关于 C++ 函数重载的例子:

1.31 重载基本类型

#include <iostream>// 重载函数,处理整数
void printValue(int x) {
   std::cout << "Integer: " << x << std::endl;
}
​
// 重载函数,处理浮点数
void printValue(double x) {
   std::cout << "Double: " << x << std::endl;
}
​
int main() {
   printValue(5);      // 调用第一个重载函数
   printValue(3.14);   // 调用第二个重载函数
​
   return 0;
}

1.32 重载函数参数个数

#include <iostream>

// 重载函数,一个参数的版本
void add(int x) {
    std::cout << "Single value: " << x << std::endl;
}

// 重载函数,两个参数的版本
void add(int x, int y) {
    std::cout << "Sum: " << x + y << std::endl;
}

int main() {
    add(5);        // 调用第一个重载函数
    add(3, 7);     // 调用第二个重载函数

    return 0;
}

1.33 重载返回类型

#include <iostream>

// 重载函数,返回整数
int add(int x, int y) {
    return x + y;
}

// 重载函数,返回浮点数
double add(double x, double y) {
    return x + y;
}

int main() {
    int result1 = add(5, 3);          // 调用第一个重载函数
    double result2 = add(2.5, 3.7);   // 调用第二个重载函数

    std::cout << "Result 1: " << result1 << std::endl;
    std::cout << "Result 2: " << result2 << std::endl;

    return 0;
}

1.4 引用

1.41 使用场景1:不影响生命周期,才可以使用引用返回

先看一个场景 Count返回n时候先把n赋给一个临时变量,再将函数Count()空间销毁,由临时变量将值赋给ret

如果使用的是引用返回,就会返回一个别名(就相当于n直接返回。就减少了拷贝),就可以直接赋值过去

出了作用域,不影响生命周期,就可以使用引用返回

#include <iostream>
#include <cstdio>

using namespace std;

//int Count(){
int& Count(){
    static int n = 0;
    n++;

    return n;
}

int main()
{
    int ret = Count();
    return 0;
}

1.42 使用场景2 :局部变量的返回

注:这是一种错误写法

#include <iostream>
#include <cstdio>

using namespace std;

int& Add(int a, int b)
{
    int c = a + b;
    return c;
}

int main()
{
    int& ret = Add(1 , 2);
    Add(3, 4);
    cout << "Add(1, 2) is : " << ret << endl;
    cout << "Add(1, 2) is : " << ret << endl;
    return 0;
}

为什么错:此处的c是一个局部变量,返回时候返回的是c的别名。当函数返回时,局部变量c被销毁,引用的就是一个不存在的内存地址

所以此处不应该用引用返回而应该是传值返回 对象还在的情况(静态,全局,上一层栈帧,malloc动态申请的等等,也就是还没还给系统的)可以使用引用返回;如果已经返还给系统了,则一定要用传值返回

像此处返回的是局部变量,结果就是未定义的

1.43 引用权限不能放大

指针和引用,赋值/初始化,权限可以缩小和保持,但是不可以放大(只适用于指针和引用)

#include <iostream>
#include <cstdio>

using namespace std;

int main()
{
    int a = 1;
    int& b = a;

    const int c = 1;
    //int& d = c; //权限放大
    const int& d = 1;

    return 0;
}

1.44 场景4 : 临时变量具有常性

临时变量具有常性,需要有const才可以编过

#include <iostream>
#include <cstdio>

using namespace std;

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

int main()
{
    //int& ret = Add(1,2);
    const int& ret = Add(1,2);
    return 0;
}

1.45 场景5 :数据类型的转换

类型转换都会产生临时变量, 而根据场景4,临时变量具有常性

#include <iostream>
#include <cstdio>

using namespace std;

int main()
{
    int a = 10;
    //double& c = a;
    const double& c = a;
    return 0;
}

1.46 引用和指针的区别

引用和指针是 C++ 中两种用于处理内存和变量间关系的不同机制。以下是它们之间的一些主要区别:

  1. 语法:

    • 引用使用 & 符号,声明时需要初始化,且一旦引用被初始化,它将一直引用同一个对象。
    • 指针使用 * 符号,声明时可以不初始化,且指针可以在之后指向不同的对象。
    cppCopy code
    int x = 10;
    int& ref = x;  // 引用
    int* ptr = &x; // 指针
    
  2. 初始化:

    • 引用必须在声明时进行初始化,且初始化后不能改变引用的目标。
    • 指针可以在声明时初始化,也可以在之后指向其他对象。
    cppCopy code
    int y = 20;
    int& ref2 = y;  // 合法
    int* ptr2;      // 合法,但未初始化
    ptr2 = &y;      // 初始化指针,指向变量
    
  3. 空引用和空指针:

    • 引用不能是空的,必须引用一个已存在的对象。
    • 指针可以是空指针(nullptr),表示不指向任何对象。
    cppCopy code
    int& ref3;   // 错误,引用必须初始化
    int* ptr3 = nullptr;  // 合法,指向空指针
    
  4. 引用不能改变目标:

    • 一旦引用被初始化,它将一直引用同一个对象,无法改变引用的目标。
    • 指针可以通过赋值操作改变指向的对象。
    cppCopy code
    int a = 5, b = 10;
    int& ref4 = a;
    ref4 = b; // 这不是改变引用目标,而是将 a 的值改为 b 的值
    
  5. 空引用和野指针:

    • 引用不能是空的,不会产生野引用的问题。
    • 指针可能成为野指针,指向未知或已释放的内存。
    cppCopy code
    int& ref5 = *(int*)0;  // 编译错误,无法创建空引用
    int* ptr4 = nullptr;
    int& ref6 = *ptr4;     // 编译通过,但可能引发运行时错误(野指针)
    

区别:

  1. 引用概念上是变量的别名,指针存的是变量地址
  2. 引用定义时候必须初始化,指针没有
  3. 引用在初始化引用一个实体过后,不能引用其他实体,而指针可以在任何时候指向任何同一类型实体
  4. 没有NULL引用,但有NULL指针
  5. 使用sizeof的时候,引用结果是引用类型的大小,指针则是地址空间所占字节数
  6. 有多级指针,没有多级引用
  7. 访问实体方式不同,指针需要显示解引用,引用编译器自己处理
  8. 总体来说,引用比指针更安全

1.5 C++推荐的替换

c++ 推荐 使用const 和 enum 去替换常量 inline 去替换 宏函数

宏缺点:

  1. 不能调试
  2. 没有类型安全的检查
  3. 有些场景非常复杂,不易掌握

宏最大的优点:不回建立栈帧,消耗低

练习:实现一个ADD宏函数

#include <iostream>
#include <cstdio>

using namespace std;

#define ADD(x, y) ((x) + (y))

int main()
{
    cout << ADD(1,2) <<endl;

    return 0;
}

inline(内联)替代宏 内联是一种空间(编译出来的可执行程序)换时间的做法

内联只是一种建议

#include <iostream>
#include <cstdio>

using namespace std;

inline int Add(int x,int y){
    int z = x+ y;
    return z;
}

int main()
{
    int ret = Add(1, 2);
    cout << ret << endl;
    return 0;
}

内联不建议声明和定义分离,会出现链接错误(因为内联函数不产生符号表)

// 内联函数的声明和定义一般放在一起
inline int add(int a, int b) {
    return a + b;
}
//只有声明 .h
//inline int add(int a, int b);

int main() {
    int result = add(3, 4);  // 编译时直接替换为实际的加法操作
    return 0;
}

1.6 auto关键字(c++11)

在 C++ 中,auto 是一个关键字,用于自动推断变量的类型。auto 的作用主要体现在以下几个方面:

  1. 类型推断: auto 使编译器能够根据变量的初始化表达式推断出变量的类型,从而简化代码。

    cppCopy code
    auto x = 42;           // 推断为 int
    auto y = 3.14;         // 推断为 double
    auto name = "John";    // 推断为 const char*
    
  2. 容器迭代器: 在使用容器迭代器时,auto 可以帮助避免繁琐的类型声明。

    cppCopy code
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    
    // 使用 auto 简化迭代器声明
    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        std::cout << *it << " ";
    }
    
  3. 复杂类型简化: 对于一些比较复杂的类型,使用 auto 可以减少代码的冗余。

    cppCopy code
    std::map<std::string, std::vector<int>> data;
    
    // 使用 auto 简化迭代器和类型声明
    for (auto& entry : data) {
        std::cout << "Key: " << entry.first << ", Values: ";
        for (auto& value : entry.second) {
            std::cout << value << " ";
        }
        std::cout << std::endl;
    }
    
  4. 模板编程: 在模板编程中,auto 可以帮助简化代码并提高代码的通用性。

    cppCopy code
    template <typename T, typename U>
    auto multiply(T x, U y) -> decltype(x * y) {
        return x * y;
    }
    
    int main() {
        auto result = multiply(3, 4.5);  // 推断为 double
        return 0;
    }
    

总的来说,auto 的作用是在不失代码可读性的前提下,简化代码,减少重复,提高代码的灵活性和通用性。然而,过度使用 auto 也可能导致代码可读性下降,因此需要谨慎使用。

1.7 范围for(c++ 11)

C++ 中的范围-based for 循环(也称为范围for循环或foreach循环)用于遍历容器中的元素,以简化迭代器的使用。以下是一些范围for循环的例子和使用方法:

1. 遍历数组:

cppCopy code
#include <iostream>

int main() {
    int numbers[] = {1, 2, 3, 4, 5};

    // 使用范围for循环遍历数组
    for (int num : numbers) {
        std::cout << num << " ";
    }

    return 0;
}

2. 遍历容器:

cppCopy code
#include <iostream>
#include <vector>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};

    // 使用范围for循环遍历容器
    for (int num : numbers) {
        std::cout << num << " ";
    }

    return 0;
}

3. 修改容器中的元素:

cppCopy code
#include <iostream>
#include <vector>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};

    // 使用范围for循环修改容器中的元素
    for (int& num : numbers) {
        num *= 2;
    }

    // 输出修改后的容器内容
    for (int num : numbers) {
        std::cout << num << " ";
    }

    return 0;
}

4. 遍历字符串:

cppCopy code
#include <iostream>
#include <string>

int main() {
    std::string message = "Hello, C++";

    // 使用范围for循环遍历字符串
    for (char ch : message) {
        std::cout << ch << " ";
    }

    return 0;
}

5. 多维数组:

cppCopy code
#include <iostream>

int main() {
    int matrix[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};

    // 使用范围for循环遍历二维数组
    for (const auto& row : matrix) {
        for (int num : row) {
            std::cout << num << " ";
        }
        std::cout << std::endl;
    }

    return 0;
}

范围for循环提供了一种简洁而直观的方式来遍历容器和数组的元素,使得代码更易读和编写。在使用时,注意避免在循环体中修改被遍历的容器,以免引起意外行为。

1.8 nullptr (c++11)

由于历史定义的原因,NULL在c++中是0,没有任何类型,而在C中是((void *)0)

c++11 使用nullptr来表示空,语法原生支持,不会被识别成int