C++基础语法学习(一)

举报
lutianfei 发表于 2022/04/10 17:04:18 2022/04/10
【摘要】 一、命名空间在大型项目中,为了避免变量或者函数名的冲突,采用命名空间对不同的项目做管理。使用命名空间,只需要把需要限制范围的代码用namespace name {} 括起来就好那么在命名空间以外使用命名空间的代码时,只需要在代码前添加:name::,例如name::code;eg:#include <iostream>using namespace std;namespace first_...

一、命名空间

在大型项目中,为了避免变量或者函数名的冲突,采用命名空间对不同的项目做管理。使用命名空间,只需要把需要限制范围的代码用namespace name {} 括起来就好那么在命名空间以外使用命名空间的代码时,只需要在代码前添加:name::,例如name::code;

eg:

#include <iostream>
using namespace std;

namespace first_space {
    void func()
    {
        cout << "Inside first_space" << endl;
    }
}
namespace second_space {
    void func()
    {
        cout << "Inside second_space" << endl;
    }
}
int main()
{
    first_space::func();
    second_space::func();
    return 0;
}

1.1 命名空间嵌套使用

命名空间是可以嵌套使用的,和一般用法一样,使用的时候需要加上name::,如果在最外层使用变量的话,就需要加上name1::name2::

eg:

namespace name1 {
    namespace name2 {
        int val = 1;  // 第2层namespace的value
    };

    // 第1层namespace的value获取第2层的value只需要加第2层namespace的name
    int val2 = name2::val;
};

// namespace以外获取第2层的value需要加两个name::,表示是name1命名空间里面的name2命名空间下的val
int val3 = name1::name2::val;

// 最外层获取第1层namespace,也只需要加第1层namespace的name::
int val4 = name1::val2;

1.2 命名空间别名

在项目非常大的时候,命名空间本身就可能会出现名字冲突,所以命名空间的名字要尽可能详细。可是当命名空间的名字很长时,使用起来会非常复杂,这个时候就可以定义命名空间别名,用一个简短的名字代替长名字

eg:

namespace AVeryVeryLongName {
    namespace AnotherVeryVeryLongNameForTest {
        int val = 1;
    };
    int val2 = AnotherVeryVeryLongNameForTest::val;
};
int val3 = AVeryVeryLongName::AnotherVeryVeryLongNameForTest::val;
int val4 = AVeryVeryLongName::val2; // 如果每一个变量都这样写,所有的赋值代码就太长了 X
                                    // 定义命名空间别名
namespace Long = AVeryVeryLongName::AnotherVeryVeryLongNameForTest;
namespace Long2 = AVeryVeryLongName;
int val5 = Long::val;
int val6 = Long2::val2;  // 使用简短的命名空间别名    √

1.3 using关键字使用

有时候,有的文件只会用到某一个或者某几个命名空间的变量,而且这几个命名空间中的变量/类/函数确定不会出现名字冲突,那就可以使用using namespace xxx(命名空间名字);

#include <iostream>
using namespace std;
int main()
{
    cout << "using namespace std" << endl;
}

1.4 匿名命名空间

当命名空间的内容只想在本文件使用的时候,可以使用匿名命名空间,编译器会给这个匿名命名空间一个唯一的名字,并且在本文件中使用using namespace。这样本文件也就不需要添加命名空间名字了。匿名命名空间和static的作用相似,但是看起来更加简洁。

namespace { // 没有名字
    int i = 0;
    std::string s = "in the namespace";
    int func()
    {
        return 10;
    }
}
int main()
{
    std::cout << i << std::endl;  //不需要添加name::
    std::cout << s << std::endl;
    std::cout << func() << std::endl;
}

二、指针与引用

什么时候是取地址符,什么时候是引用

2.1 指针与引用基础

参考资料:

2.1.1 指针

指针是一个变量,它的值是另一个变量的地址,在32位系统上是4字节,64位操作系统上是8字节
使用 星号*可以访问指针所在内存地址中存的值

eg:

int var = 20;
int *ip = &var;

image.png

*符号的说明

*号在用于表明指针时有两种用法:

  1. 指针定义时, *号结合前面的类型表明要定义的变量为指针及其类型
  2. 指针的解引用,解引用时*p表示 p指向的变量本身。

eg:

int mian(void)
{
          int *p;
          int*  p1;
          int* p2;
          int * p3;//编译后发现四种定义指针的方式编译器认为是等价的.
 //当我们定义两个指针变量时:
          int *p4, *p5;//这样才是定义了两个指针变量。
          int *p4, p5;//此时p4是指向int的指针而p5是int类型变量。
//为了避免定义多个指针变量出现歧义的情况采取第一种 int *p的定义方式。
}



int main(void)
{
            int a=23;
            int b=0;
            int *p;//*的第一种用法
            p=&a;
            b=*p;//*的第二种用法
            printf("b=%d.\n",b);
}

2.1.2 引用

引用是已存在变量的另一个名字,指向同一块地址,如果有一个发生变化,另一个也一定发生变化
引用和指针的区别:一定不存在空引用,无法改变指向对象,必须在被创建时初始化

eg:

int var = 20;
int &r = var;

image.png

&符号的说明

引用中的&只会出现在定义中 , 在变量类型(类)名后面(与指针相似) , 并且在接下来的代码中 , &符号都不会再出现 , 只需直接使用引用定义的名字即可
而取地址符是不用绑定类名的 , 直接在变量(对象)左边写上& 即可(变量可以是指针或引用 , 毕竟它们也算是变量)。

image.png

2.1.3 指针与引用的区别

  • 总论
引用 指针
必须初始化 可以不初始化
不能为空 可以为空
不能更换目标 可以更换目标
  1. 引用必须初始化,指针可以不初始化
    eg:
int &r; //不合法,没有初始化
int *p; //合法,但p为野指针,使用时需要小心处理
  1. 引用不能为空,而指针可以为空
    由于引用不能为空,所以我们在使用引用时不需要测试其合法性,而使用指针时需要首先判断指针是否为空,否则会引起程序崩溃。
    eg:
void test_p(int* p)
{
  	if(p != null_ptr)    //对p所指对象赋值时需先判断p是否为空指针
    	*p = 3;
    return;
}

void test_r(int& r)
{
    r = 3;    //由于引用不能为空,所以此处无需判断r的有效性就可以对r直接赋值
    return;
}
  1. 引用不能更换目标
    指针可以随时改变指向,而引用只能指向初始化时指向的对象,无法改变。
    说明:虽然引用不可以改变指向,但是可以改变初始化对象的内容。例如就++操作而言,对引用的操作直接反应到所指向的对象,而不是改变指向;而对指针的操作,会使指针指向下一个对象,而不是改变所指对象的内容。
    eg:
int a = 1;
int b = 2;

int &r = a; //初始化引用r指向变量a
int *p = &a; //初始化指针p指向变量a

p = &b; //指针p指向了变量b
r = b; //引用r依然指向a,但a的值变成了b
  1. 引用的大小是所指向的变量的大小,因为引用只是一个别名而已;指针是指针本身的大小,4或8个字节

一句话归纳为就是:指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名,引用不改变指向。

2.1.4 指针与引用各自的应用场景

  1. 指针参数可以在调用时可以传递 NULL 而引用不可以。所以如果你的参数是可选的话选择传递指针。
  2. 如果需要传递数组参数的话,只能使用指针(原因:https://www.cnblogs.com/pacino12134/p/11277153.html)
  3. 当需要在函数中重新绑定参数时使用指针
  4. 指针参数在调用的时候会比引用要明显一些
    eg:
int fun(val); //难以看出是传递引用还是直接传递值
int fun(&val); //很明显是传递指针
  1. 其他情况尽可能的使用引用。因为引用从语义上来说更直白一些,也更不容易出错。 而且引用一定是指向一个合法的对象,而指针需要在使用之前检查是否为 NULL。

2.2 智能指针

2.2.1 智能指针概述

手动管理动态内存的方式很容易出错,常见的问题:

  • 忘记delete内存。导致内存泄漏
  • 使用已经释放掉的对象
  • 重复释放同一内存

C++11开始引入智能指针,可以更方便的管理内存。与传统指针相比,智能指针负责自动释放所指向的对象,C++标准库定义了两种智能指针:

  • unique_ptr 独占所指向的对象
  • shared_ptr 允许多个指针指向同一个对象
    以及一个weak_ptr伴随类,指向shared_ptr所管理的对象。

指针都定义在头文件<memory>

两种指针都支持的操作:
image.png

image.png

2.2.2 智能指针与异常的关系

使用智能指针的好处之一是即使程序发生异常,内存也能够被回收。
eg:
手动创建指针
image.png

智能指针
image.png
函数的退出有两种情况,正常结束或者发生异常,无论哪种情况,局部对象都会被销毁,而智能指针作为局部对象被销毁时会检查资源使用情况,并正确释放

2.2.3 unique_ptr

独占所指向的对象

  • 某个时刻只能有一个unique_ptr指向一个给定的对象
  • 当unique_ptr销毁时,它所指向的对象也被销毁

支持的操作如下:
image.png

demo

#include <memory>
#include <cassert>
#include <iostream>
#include <fstream>

struct D {
    D() { std::cout << "D::D\n"; }

    ~D() { std::cout << "D::~D\n"; }

    void bar() { std::cout << "D::bar\n"; }
};

//a function consuming a unique_ptr can take it by value or by rvalue reference
std::unique_ptr<D> pass_through(std::unique_ptr<D> p) {
    p->bar();
    return p;
}

//helper function for the custom deleter demo bellow
void close_file(std::FILE *fp) {
    std::fclose(fp);
}


int main() {
    setbuf(stdout, NULL);
    std::cout << "unique ownership semantics demo\n";
    {
        auto p = std::make_unique<D>(); // p is as unique_ptr that owns a D
        auto q = pass_through(std::move(p));
        assert(!p); // now p owns nothing and holds a null pointer
        q->bar(); // q owns the D object
    } // ~D called here

    std::cout << "Custom deleter demo\n";
    std::ofstream("demo.txt") << 'x'; // prepare the file to read

    {
        std::unique_ptr<std::FILE, decltype(&close_file)> fp(
                std::fopen("demo.txt", "r"), &close_file);

        if (fp) {
            // fopen could have failed; in which case fp holds a null pointer
            std::cout << (char) std::fgetc(fp.get()) << '\n';
        }
    } // fclose() called here, but only if FILE* is not a null pointer
}


//运行结果
// unique ownership semantics demo
// D::D
// D::bar
// D::bar
// D::~D
// Custom deleter demo
// x

2.2.4 shared_ptr

shared_ptr内部使用引入计数的方式管理资源,如果资源的计数值变为0,则释放资源

image.png

image.png

使用make_shared的几个好处
  1. 写法更简洁
    image.png

  2. 提升异常安全
    比如函数void processWidget(std::shared_ptr<Widget> spw, int priority);如果用户传递的参数是下面这种,则可能会存在内存泄漏:
    processWidget(std::shared_ptr<Widget>(new Widget), computePriority());
    因为:在执行processWidget函数体之前会有以下三步:

    1. new Widget 分配Widget对象
    2. 构造shared_ptr去管理对象
    3. 执行computePriority()函数

而编译器在生成的时候,只会保证1在2之前执行,如果生成的顺序是1->3->2,并且执行3时发生异常则1分配的对象就没法释放
因此合理的写法如下:
processWidget(std::make_shared<Widget>(), computePriority()); //正解

  1. 生成的代码更小、更快
  • 比如std::shared_ptr<Widget> spw(new Widget);这里需要做两次分配,一次在堆上分配Widget对象,一次分配ControlBlock
  • auto spw = std::make_shared<Widget>();make_shared分配一块内存同时保存Widget对象与控制块。
make_shared不能适用的场景

用户指定deleters
传递初始化列表

注:优先使用make_shared而不是new,这里的讨论同样适用与make_unique

2.2.5 weak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针

  • 它指向由一个shared_ptr管理的对象
  • 不会改变绑定的shared_ptr的引用计数
  • 即使有weak_ptr指向对象,对象还是会被释放

image.png

weak_ptr用途

  • 防止shared_ptr出现引用计数循环引用,导致资源无法释放的问题
  • 临时使用权:当一个对象随时可能被删除,并且只有当对象存在时才能被访问,可以使用weak_ptr获得临时使用权(转成shared_ptr)类型
#include <memory>
#include <iostream>

std::weak_ptr<int> gw;

void observe() {
    std::cout << "use_count == " << gw.use_count() << ": ";
    if (auto spt = gw.lock()) {
        //Has to be copied into a shared_ptr before usage
        std::cout << *spt << "\n";
    } else {
        std::cout << "gw is expired\n";
    }
}

int main() {
    setbuf(stdout, NULL);
    {
        auto sp = std::make_shared<int>(42);
        gw = sp;
        observe();
    }

    observe();
}



// 运行结果:
// use_count == 1: 42
// use_count == 0: gw is expired
using Ptr = std::shared_ptr<Instance>;
static Ptr create(bool enableValidationLayer) { return std::make_shared<Instance>(enableValidationLayer); }

三、 一些关键字和符号说明

3.1 auto

参考资料:
c++ auto类型用法总结

auto是c++程序设计语言的关键字。用于两种情况

  1. 声明变量时根据初始化表达式自动推断该变量的类型
    eg: 对于值x=1;既可以声明: int x=1 或 long x=1,也可以直接声明 auto x=1。但是,这么简单的变量声明类型,不建议用auto关键字,而是应更清晰地直接写出其类型。
  2. 声明函数时函数返回值的占位符

auto关键字实际更适用于:

  1. 类型冗长复杂、变量使用范围专一时,使程序更清晰易读。
    eg:
std::vector<int> vect;
for(auto it = vect.begin(); it != vect.end(); ++it)
{  //it的类型是std::vector<int>::iterator
    std::cin >> *it;
}
  1. 保存lambda表达式类型的变量声明
    eg:
auto ptr = [](double x){return x*x;};//类型为std::function<double(double)>函数对象

3.2 default

显式的要求编译器生成函数的一个默认版本。

比如说下面的代码,如果用户写了一个构造函数A(int){};,那么就不会为class A生成一个默认的构造函数。这个时候如果用户还需要一个和编译器自动生成的一模一样的默认的构造函数,那么就可以使用=default关键字,显式的让编译器生成一个。

class A{
public:
    A(int){};
    A() = default;
    //A(){}; // old way to get an empty constructors like default one.
};

3.3 NULLnullptr

参考资料:
为什么建议你用nullptr而不是NULL
NULL,0,\0的区别

NULL:在C++中的定义为#define NULL 0,并且在c++中不能将void *类型的指针隐式转换成其他指针类型,所以不能将NULL定义为(void*)0

nullptr:nullptr并非整型类别,甚至也不是指针类型,但是能转换成任意指针类型。nullptr的实际类型是std:nullptr_t,c++ 11之后的空指针表示尽量用 nullptr。

3.4 [[nodiscard]] 符号

参考资料:
nodiscard介绍 C++

nodiscard是c++17引入的一种标记符,其语法一般为[[nodiscard]]或[nodiscard(“string”)],含义可以理解为“不应舍弃”。nodiscard一般用于标记函数的返回值或者某个类,当使用某个弃值表达式而不是cast to void 来调用相关函数时,编译器会发出相关warning。

[[nodiscard]] int func(){return 1;}; // C++17
[[nodiscard("nodiscard_func_1")]] int func_1(){return 2;};  // C++20

func(); // warning
func_1(); // warning

此时编译器会有如下告警:

warning C4834: 放弃具有 "nodiscard" 属性的函数的返回值
warning C4858: 正在放弃返回值: nodiscard_func_1

改为如下形式则不再告警:

[[nodiscard]] int func(){return 1;}; // C++17
int a = func(); // no warning
static_cast<void>(func()); // no warning

3.5 using关键字

参考资料:
C++ using关键字作用总结
C++中的using关键字

  1. using namespace

  2. 在子类里使用父类的private成员

  3. 重命名(更强大的typedef用法)

typedef long long ll; //等价于
using ll=long long;

3.6 回调函数

参考资料:
C++回调函数的理解与使用

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
回调函数机制:

  1. 定义一个函数(普通函数即可);
  2. 将此函数的地址注册给调用者;
  3. 特定的事件或条件发生时,调用者使用函数指针调用回调函数。
【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。