Skip to content

C++初级

刚学 C++的时候的基本笔记

auto

暂时仅考虑 vector 的情况

auto 会创建拷贝,如果对象较小,不需要修改则使用

vector<int> vec;
for (auto value : vec)
{
    value++; //对原始 vec 无影响
}

auto& 会创建指向原始对象的引用,需要修改或者是复杂就用

vector<int> vec;
for (auto& valut : vec)
{
    value++; //对原始 vec 有影响
}

在这里,这是一个 C++11 的基于范围的 for 循环,可以直接引用容器内的元素

因此 value 的类型是:int 或者 int&,而不是迭代器

在这里,auto 和 auto&的区别只是

auto 不能修改

auto& 可以修改

迭代器

迭代器是一种广义化的 指针,它使得 C++ 程序可以通过统一的方式处理不同的数据结构,它用于指向容器中的元素

xxx.begin() xxx.end() 返回的都是一个“指针”

*it 通过解引用操作访问迭代器当前指向的元素

最正常的例子:

for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
    std::cout << *it << " ";  // 输出:1 2 3 4 5
} //需要* it = 10;
//需要解引用

迭代器的意义上给 list 这种双向链表也提供了统一的接口

对于 vector 来讲,迭代器和用下标访问差不多,因为它能随机访问,list 不行

STL 和 STD 是什么关系

STL(Standard Template Library,标准模板库)是标准库的子集

std 命名空间下的所有代码被称为标准库

虚函数与纯虚函数

拥有函数的类相当于 Java 中的父类,而拥有纯虚函数的类相当于 Java 中的接口类

只要一个类有纯虚函数(=0),那么整个类就虚拟了

纯虚函数可以不继承,直接在本类的 cpp 里实现

一个函数声明为函数,表示开发者,允许你写的子类,重写这个方法,如果不是的,表示不建议重写

虚函数表

每个包含虚函数的类(直接 or 间接发生关系),编译器会为它生成一张 虚函数表。这张表本质上是一个函数指针数组,存储了该类所有虚函数的实际入口地址。

vtable 的数据在编译时完全确定,运行期间不会改变。因此,它被放在进程地址空间的 只读数据段 (.rodata) 中

一个类只有一个 vtable(不按对象计),同一类的所有对象共享这个 vtable

每个包含虚函数的类对象,在实例化时都会被编译器隐式地插入一个 虚函数指针(通常叫 __vptr)。这个指针指向该类对应的虚函数表(每个对象的动态类型在运行时可能不同,因为同一个基类指针可以指向不同类型的对象)

vptr 是对象实例的一部分,因此它的位置完全取决于 对象实例的存储位置

头文件应当自包含(不是循环依赖)

可以肯定的一点是,#include 确实是复制文本

但是一个良好的行为是:一个头文件,不应该不包含 vector 而使用 vector

而是在引入它的所有 cpp 里,比该头文件更早的引入 vector

这样做确实可以编过,并且没有 intellisense 错误,但是并不推荐

此问题常见于发生:在编写头文件时,intellisence 报错,原因是没还没写对应的 Cpp 文件,而 Cpp 文件需要先引入 stdafx.h

循环依赖

https://thysrael.github.io/posts/4d93d9f0/

C
#pragma once
#include "b.h"

struct A {
    struct B *b;
};
C
#pragma once
#include "a.h"

struct B {
    struct A *a;
};

这是循环依赖的问题。

解决:

使用前向声明,将其中一个 include 的头文件去掉(这里是 B),同时声明另一个类

C
class A; // forward declaration

struct B {
    struct A *a;
};

到目前,出现的文件都是头文件,这里不建议使用函数。

并且 struct A* a 必须是指针,而不是变量,即 struct A a

因为此时编译器要确定 B 的内存大小,必须指针才能固定大小(C++里,可以是 A&)

写代码的时候的一个范式,就是尽量减少 h 文件中的 include ,而在 c 中 include 文件

它和前一条的自包含并不矛盾,即头文件要对自己真正使用到的信息负责,但不要多负责

在 .h 里,优先前向声明,减少不必要的 include

不要依赖调用者先 include 某个东西

自包含 struct(C)

Struct 文法定义:

struct [tag] {member-list} variable-list ;

这意味着在没有 typedef 的情况下,{}后面的内容是局部变量名

如果不给出 tag,说明是匿名结构体

tag 是这个结构体的类名(结构体名字)

结构体的自引用:

c
struct my_struct{
    int a;
    struct my_struct b;
}

这个不合法,因为无限制的大小

必须是 struct my_struct* b; 内存大小确定

用 typedef 构造自引用结构体:

c
typedef struct{
    int a;
    StackNode* b;
} StackNode;

typedef struct StackNode_{
    int a;
    StackNode_* b;
}StackNode;

第一个失败了,解析结构体内部时,StackNode 对于编译器而言,是未知的,因为当前结构体是匿名的,而必须完成当前结构体的完整定义,才能 typedef 为 StackNode,这产生了矛盾。

而第二个就可以,因为编译器知道当前结构体就叫做 StackNode_ ,而 StackNode_指针的大小也是固定的。在整体定义好以后,才起别名。

类与编译通过

一个正常的行为是,一个类声明在头文件里,然后在对应的同名源文件里实现它的函数,其它源文件使用时,引入头文件。 由于引入了头文件,编译时,会有类的声明,然后链接时,编译器会找到另一个编译单元的类的函数的实现,帮你跨编译单元调用过去,并整合为一个编译产物。

在一个编译单元中,一个类,不能声明两次。(注意我的用词,是声明)

一个类,如果它的函数,是独立实现(即 MyClass::run() 这种的),那么只能在所有被链接的编译单元里,也只能出现一次。

但是,一个类,如果如果它直接实现在头文件里了(即函数直接实现在头文件里),被引入多个源文件,是可以工作的。(你可以考虑有一些类,它的析构函数,直接就是~MyClass(){} 直接就是一个空实现,这不会导致问题)

影响力问题(拷贝)

关于函数返回指针的问题

返回指针,你一般可以做到:

  • 返回的是全局变量,这个全局变量大概率是一个容器,容器内部存储对象,返回某个对象的指针
  • 返回的是形参传入的指针相关,改改 不能做:
  • 在函数内部,构造一个局部变量,然后取它的地址返回

关于函数形参有指针:

因为 java 是这样传参的,如果参是 int,则传值,如果参是对象,则传地址

那么在 C 中模仿 java 的行为

你想修改一个结构体,就必须要形式参数是结构体指针

说白了,你怕的不就是这个嘛:

java 里,外部的对象传入函数了,是可以修改的,而 c++传对象本身是拷贝,不让改

java 里改,外部对象,也可以指向函数内部 new 出的对象 而 C++影响不到外面

即 Java:ArrayList<String> 和 C++ std::vector<std::wstring>行为不一致

行为一致的是 std::vector<std::wstring>&

在 C 语言中,如果我想要让一个函数返回一个结构体(或者用函数初始化一个结构体)

(1)返回一个结构体,利用拷贝,将结构体复制给外面的结构体变量 这样做最直观,但是结构体拷贝占用大量资源

(2)返回结构体指针,在函数内使用内存分配,后续在外面使用 外面的函数要去释放内存,心智负担重

(3)改写函数,反而要求形参是一个结构体指针,然后传入一个已有的结构体,进行修改

这个是最正常的做法了 不管是 C 还是 C++,甚至 C#,基本上都是这个办法

C 是要去结构体指针

C++可能是结构体引用

C#是 ref