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/
#pragma once
#include "b.h"
struct A {
struct B *b;
};#pragma once
#include "a.h"
struct B {
struct A *a;
};这是循环依赖的问题。
解决:
使用前向声明,将其中一个 include 的头文件去掉(这里是 B),同时声明另一个类
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 是这个结构体的类名(结构体名字)
结构体的自引用:
struct my_struct{
int a;
struct my_struct b;
}这个不合法,因为无限制的大小
必须是 struct my_struct* b; 内存大小确定
用 typedef 构造自引用结构体:
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