Skip to content

C++ Basics Review

· 23 min

Fundamental Types And Compound Types#

Integer#

Char#

Signed Integers Overflow#

int a = std::numeric_limits<int>::max();
int b = 1;
int result = a + b;
std::println("Result of a + b: {}", result);

Unsigned Integer is Always >= 0#

Integer Promote#

uint32_t a32 = 0xffffffff, b32 = 0x00000001, c32 = 0xffffffff;
uint8_t a8 = 0xff, b8 = 0x01, c8 = 0xff;
std::println("sizeof(int) : {}", sizeof(int));
std::println("uint32_t : {}", a32 + b32 > c32 ? "unexpected!" : "ok");
std::println("uint8_t : {}", a8 + b8 > c8 ? "unexpected!" : "ok");

The Size of Integers#

Bit Manipulation#

Literal Prefix and Suffix#

Bool#

Floating points#

What is it?#

Accuracy#

float a = 0.1f; // -> 0.100000001490116119384765625
double z = tan(pi/2.0); // -> 16331239353195370.0, not inf

Bit Manipulation#

Literal Prefix and Suffix#

Compound Types#

cv-qualifier#

Array-to-pointer Conversion (decay)#

void arraytest(int *a);
void arraytest(int a[]);
void arraytest(int a[3]);
void arraytest(int (&a)[3]);

VLA is Not Allowed#

Enumeration#

Expression#

Class#

Ctor and Dtor#

Initialization#

Ctor#

Copy Ctor#

// Copy Ctor
Class(const Class& another) {
}
// operator=
Class& operator=(const Class& another) {
/*do something*/
return *this;
}

Member Functions#

Inheritance#

Polymorphism#

Virtual Methods#
override and final#
Virtual Methods With Default Params#
void Parent::go(int i = 2) {
std::println("Base's go with i = {}.", i);
}
void Child::go(int i = 4) {
std::println("Derived's go with i = {}.", i);
}
int main() {
Child child;
child.go();
Parent& childRef = child;
childRef.go();
return 0;
}

Template Method Pattern and CRTP#
class Student {
public:
float getGpa() const {
return getGpaCoeff() * 4.0f;
}
private:
virtual float getGpaCoeff() const {
return 1.0f;
}
};
class Tom : public Student {
float getGpaCoeff() const override {
return 0.8f;
}
};
template <typename Derived>
class Student {
public:
float getGpa() {
return static_cast<Derived*>(this)->getGpaCoeff() * 4.0f;
}
};
class Tom : public Student<Tom> {
public:
float getGpaCoeff() const {
return 0.8f;
}
};
Pure Virtual Function#
class Base {
public:
Base() {
reallyDoIt(); // calls virtual function!
}
void reallyDoIt() {
doIt();
}
virtual void doIt() const = 0;
};
class Derived : public Base {
void doIt() const override {}
};
int main() {
Derived d; // error!
return 0;
}

Struct#

Function Overloading#

Operator Overloading#

Ambiguity#

class Real {
public:
Real(float f) : // use explicit to avoid ambiguity
val { f } {
}
Real operator+(const Real& a) const {
return Real{val + a.val};
}
operator float() {
return val;
}
private:
float val;
};
int main() {
Real a { 1.0f };
Real b = a + Real{0.1f}; // ok
// Real b = a + 0.1f; // error, ambiguous
return 0;
}

Three-way Comparison <=>#

struct Data {
int id;
float val;
auto operator<=>(const Data& another) const {
return val <=> another.val;
}
bool operator==(const Data& another) const {
return val == another.val;
}
};
int main() {
Data a { 0, 0.1f };
Data b { 1, 0.2f };
a < b; a <= b; a > b; a >= b; // boost by <=>
a == b; a != b; // boost by ==
return 0;
}

Lambda Expression#

int main() {
int copy { 0 };
int ref { 0 };
auto f {
[c = copy, &ref]() {
return c + ref;
}
};
f();
return 0;
}
// same as
int main() {
int copy { 0 };
int ref { 0 };
class __lambda_5_9 {
public:
inline /*constexpr */ int operator()() const {
return c + ref;
}
private:
int c;
int& ref;
public:
__lambda_5_9(int& _c, int& _ref): c { _c }, ref { _ref } {}
};
__lambda_5_9 f = { __lambda_5_9 { copy, ref } };
f.operator()();
return 0;
}

Improvement in Execution Flow#

enum class VeryLongName {
LONG,
LONG_LONG,
LONG_LONG_LONG,
};
auto val = VeryLongName::LONG;
switch (val) {
using enum VeryLongName;
case LONG:
foo();
[[fallthrough]];
case LONG_LONG:
foo();
break;
case LONG_LONG_LONG:
foo();
break;
default:
break;
}

Template#

void foo(const auto& a, const auto& b) {}
// same as
template <typename T1, typename T2>
void foo(const T1& a, const T2& b) {}
auto less = [](const auto& a, const auto& b) static { return a < b; };
auto less = []<typename T>(const T& a, const T& b) static { return a < b; };

Homework#

上半部分#

1#

使用gcc(善用compiler explorer)编译并运行下面的程序:

#include <limits>
int main()
{
int a = std::numeric_limits<int>::max();
int b = a + 1;
return 0;
}

随后尝试加上编译选项-ftrapv,看看运行结果有没有变化。

Answer#

见笔记.

2#

Answer#

打印一个std::uint8_t的变量的地址;如果你想用print,可以使用std::println("{}", 你的地址)

uint8_t a = 0;
std::println("{}", static_cast<void *>(&a));

3#

在2001年的游戏《雷神之锤3》(Quake III Arena)中,使用了一种快速倒数平方根算法,用于对于32位浮点数x计算1/x1/\sqrt x的近似值。它的算法如下:

/* 设操作BitEquiv表示两个数在二进制表示上相同, i32表示32位整数,f32表示32位浮点数。*/
f32 GetInvSqrt(f32 num)
{
i32 a <- BitEquiv(num);
i32 b <- 0x5f3759df - (a >> 1);
f32 y <- BitEquiv(b);
return y * (1.5f - (num * 0.5f * y * y));
}

试把上述伪代码转为C++代码。可以尝试几个数,看看和<cmath>中的std::sqrtf相比的误差;如果你感兴趣,也可以在quick-bench上比一比性能。

当然,这个算法在目前是比CPU硬件指令更慢的,只是在2000年代更好。它本质上使用了牛顿迭代法,return的步骤进行了一次迭代,如果想要更高的精度可以把它赋给y,继续用该式迭代。

Answer#
float getInvSqrt(float num) {
uint32_t a = std::bit_cast<uint32_t>(num);
uint32_t b = 0x5f3759df - (a >> 1);
float y = std::bit_cast<float>(b);
return y * (1.5f - (num * 0.5f * y * y));
}
int main() {
std::println("getInvSqrt : {}", getInvSqrt(0.1f));
std::println("std::sqrtf : {}", 1.0f / std::sqrtf(0.1f));
return 0;
}

4#

写一个将二进制数据转为HTTP要求的字节序的函数。注意用std::endian区分当前机器大小端的情况;如果两个都不是,打印一个警告。

Answer#
template <std::integral T>
T toHttpData(T input) {
if constexpr (std::endian::native == std::endian::big) {
return input;
} else if constexpr (std::endian::native == std::endian::little) {
return std::byteswap(input);
} else {
std::println("warning!");
}
}
int main() {
std::println("{:#x}", toHttpData(0x112233));
return 0;
}

5#

判断以下程序的合法性:

int a[][3] = { {1, 2, 3}, {4,5,6} };
int (*b)[3] = a; // 合法吗?
int **c = a; // 合法吗?
int **d = b; // 合法吗?
int a[]{1,2,3}, b[]{4,5,6,7};
void Func(int (&a)[3]) { /* ... */}
void Func2(int a[3]) { /* ... */}
Func(a); Func2(a);
Func(b); Func2(b); // 这四个里面哪个合法?

希望你可以通过这个例子区分指针和数组,加深对数组decay的理解;指针所指向的目标不会继续decay,引用所引用的目标也会暂时保持原类型(不过仍然允许后续的decay,比如上面的Funcint* p = a;是合法的)。

Answer#
int a[][3] = { {1, 2, 3}, {4,5,6} };
int (*b)[3] = a; // 合法
int **c = a; // 不合法, a -> int[2][3]
int **d = b; // 不合法, b -> int (*)[3]
int a[]{1,2,3}, b[]{4,5,6,7};
void Func(int (&a)[3]) { /* ... */}
void Func2(int a[3]) { /* ... */}
Func(a); // 合法
Func(b); // 不合法
Func2(a); // 合法
Func2(b); // 合法

6#

写一个函数,它接受一个返回值为int、参数为floatdouble的函数指针为参数,返回void

Answer#
using Func = int(*)(float, double);
void foo([[maybe_unused]] Func func) {
std::println("do something");
}

7#

写一个scoped enumeration,它包含read, write, exec三个选项;同时为了使它们可以按位组合,编写一个配套的重载的按位与和按位或的运算符。为了一般性,可以使用auto b = std::to_underlying();如果你的编译器不支持,可以使用using T = std::underlying_type<Enum>,再手动转为T

Answer#
enum class access_t : std::uint8_t {
read = 1,
write = 2,
exec = 4,
};
access_t operator&(access_t a, access_t b) {
return static_cast<access_t>(std::to_underlying(a) & std::to_underlying(b));
}
access_t operator|(access_t a, access_t b) {
return static_cast<access_t>(std::to_underlying(a) | std::to_underlying(b));
}

8#

对于下面的程序:

#include <map>
int main()
{
std::map<int, int> m;
m[0] = m.size();
}

按照C++17标准规定,m[0]是什么?

Answer#
#include <map>
int main() {
std::map<int, int> m;
m[0] = m.size(); // m[0] -> 0,
}

下半部分#

1#

写一个Vector3类,它还有三个float分量,可以用不超过3个float进行初始化,例如:

Vector3 vec; // (0, 0, 0)
Vector3 vec2{1}; // (1, 0, 0)
Vector3 vec3{1, 2}; // (1, 2, 0)

同时,定义operator+operator+=。试一试,1+aoperator+写成成员函数时是否可用?写成全局函数时呢?给构造函数加上explicit时呢?

这是因为1作为函数的参数,进行的是copy initialization,这正是explicit所禁止的;必须写为Vector3{1}才可以。

随后,我们增加比较运算符,比较两个向量的长度。用简短的方法使六个比较运算符都有效。

别忘了用属性警告用户某些运算符的返回值不应被抛弃。

Answer#
#include <print>
#include <cmath>
class Vector3 {
public:
Vector3(float a = 0.0f, float b = 0.0f, float c = 0.0f) :
val { a, b, c } {
}
void display() const {
std::println("val: {}, {}, {}; len: {}", val[0], val[1], val[2], length());
}
[[nodiscard]]
Vector3 operator+(const Vector3& another) const {
return {
val[0] + another.val[0],
val[1] + another.val[1],
val[2] + another.val[2],
};
}
[[nodiscard]]
Vector3 operator+(const float num) const {
return {
val[0] + num,
val[1] + num,
val[2] + num,
};
}
[[nodiscard]]
friend Vector3 operator+(const float num, const Vector3& vec) {
return vec + num;
}
Vector3& operator+=(const Vector3& another) {
val[0] += another.val[0];
val[1] += another.val[1];
val[2] += another.val[2];
return *this;
}
Vector3& operator+=(const float num) {
val[0] += num;
val[1] += num;
val[2] += num;
return *this;
}
[[nodiscard]]
bool operator==(const Vector3& another) const {
return length() == another.length();
}
[[nodiscard]]
bool operator==(const float len) const {
return length() == len;
}
[[nodiscard]]
auto operator<=>(const Vector3& another) const {
return length() <=> another.length();
}
[[nodiscard]]
auto operator<=>(const float len) const {
return length() <=> len;
}
private:
float length() const {
return std::hypot<float>(val[0], val[1], val[2]);
}
private:
float val[3];
};
int main() {
Vector3 vec1;
Vector3 vec2 { 1 };
Vector3 vec3 { 1, 2 };
auto vec = vec1 + vec3;
vec.display();
vec = 1 + vec2;
vec.display();
vec += 1;
vec.display();
std::println(
"{}, {}, {}, {}, {}, {}",
vec < vec1,
1.0f <= vec1,
vec > 1.0f,
vec >= vec1,
vec == vec1,
1.0f != vec1
);
return 0;
}

2#

除了vector,其他的容器可以使用Uniform initialization吗?特别地,mapunordered_map每个元素是一个pair。

Answer#

可以

3#

有虚函数的类和普通的类相比,有大小上的差别吗?

Answer#

有, 可能可以优化到没有

4#

下面的两个函数构成重载吗?

void Test(int);
int Test(int);

下面两个呢?T = void会有干扰吗?

void Test(int);
template<typename T> T Test(int);

想一想name mangling的规则,得出你的结论,在编译期上进行测试。

Answer#
template<typename T = void>
T Test(const int a) {
if constexpr (std::is_same_v<T, float>) {
std::println("float {}", a);
return 0.0f;
} else if constexpr (std::is_same_v<T, void>) {
std::println("void {}", a);
return;
} else {
static_assert(false);
}
}
void Test(const int a) {
std::println("int {}", a);
}
int main() {
Test(1);
Test<>(2); // only for "T = void"
Test<void>(3);
Test<float>(4);
// Test<int>(5); // error
return 0;
}

5#

下面的程序中,bar返回什么是确定的吗?分别在gcc无优化选项和-O2中试试。

struct Foo
{
int foo()
{
return this ? 42 : 0;
}
};
int bar()
{
Foo* fptr = nullptr;
return fptr->foo();
}

这道题的目的是告诉你不要写UB,空指针不能解引用。

Answer#

是 UB (恼

6#

写一个三维数组,以存储的类型为模板参数,内部使用摊平的一维连续数组存储,允许使用多维下标运算符进行访问。定义它的拷贝构造函数,想想是否要考虑自赋值问题。

Answer#
#include <print>
#include <array>
template <typename T, std::size_t Dx, std::size_t Dy, std::size_t Dz>
class Array3 {
private:
constexpr static std::size_t D = Dx * Dy * Dz;
public:
Array3() = default;
Array3(const std::array<T, D>& a) :
arr { a } {
}
Array3(const std::initializer_list<T>& l) {
if (l.size() != D) {
throw std::invalid_argument("Initializer list size mismatch");
} else {
std::copy(l.begin(), l.end(), arr.data());
}
}
Array3(const Array3<T, Dx, Dy, Dz>& another) :
arr { another.arr } {
}
Array3& operator=(const Array3<T, Dx, Dy, Dz>& another) {
arr = another.arr;
return *this;
}
[[nodiscard]]
T& operator[](const std::size_t dx, const std::size_t dy, const std::size_t dz) {
if (not (dx < Dx and dy < Dy and dz < Dz)) {
throw std::out_of_range("Index out of range");
}
return arr[dx * Dy * Dz + dy * Dz + dz];
}
void display() {
for (auto& m : arr) {
std::print("{} ", m);
}
std::println("");
}
private:
std::array<T, D> arr;
};
template <std::size_t Dx, std::size_t Dy, std::size_t Dz>
using Array3f = Array3<float, Dx, Dy, Dz>;
int main() {
Array3f<1, 3, 2> a { 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f };
auto b = a;
a.display();
b.display();
b[0, 1, 0] = 3.0f;
a = b;
a.display();
b.display();
return 0;
}

不涉及指针, 没有自赋值问题.

7#

写一个函数,它接受std::vector<int>&,使用<algorithm>中的std::ranges::generate在其中填充斐波那契数列。你只需要填写第二个参数,即一个lambda表达式:

void FillFibonacci(std::vector<int>& v)
{
std::ranges::generate(v, /* 你的回答 */);
}

可以认为上面的式子相当于:

for(auto& elem : v)
elem = YourLambda();

提示:你可以在capture中创建新的变量,例如[a = 0, b = 1]

Answer#
void fillFibonacci(std::vector<unsigned int>& v) {
std::ranges::generate(v, []() {
static unsigned int a = 0;
static unsigned int b = 1;
unsigned int ret = a;
a = b;
b = ret + b;
return ret;
});
}

8#

定义一个scoped enumeration Color,有red, blue, green三个枚举量。编写一个switch,使得redgreen都打印Hello,对blue打印World。使用using enum来避免每个枚举都写Color::,并使用属性防止编译器对fallthrough给出警告。

Answer#
enum class Color {
RED,
BLUE,
GREEN,
};
void say(const Color& c) {
switch (c) {
using enum Color;
case RED :
[[fallthrough]];
case BLUE :
std::println("Hello");
break;
case GREEN :
std::println("World");
break;
default :
throw std::logic_error("");
break;
}
}

9#

对下面的结构体的数组调用sort,使用lambda表达式进行比较。

struct DijkstraInfo
{
int vertexID;
int distance; // 作为比较的标准
};
std::ranges::sort(vec, /* 你的lambda表达式 */ );

想一想,是否可以把两个参数类型都写成const auto&

如果你写成一个函数:

bool Func(const auto& a, const auto& b);

它能否像lambda表达式一样传入sort?还是需要实例化?为什么?

如果你写成一个下面的结构体:

template<typename T>
struct Functor
{
// 如果你的编译器暂时不支持static operator(),把static去掉.
static bool operator()(const T& a, const T& b){ /* ... */ }
};

它能否像lambda表达式一样传入sort?还是需要实例化?为什么?

希望你通过这个例子理解模板实例化的要求, 可以通过包装在无需实例化的实体中绕过限制, 推迟到调用的时候再实例化, 此时就不需要程序员手动实例化.

Answer#
#include <print>
#include <vector>
#include <algorithm>
struct DijkstraInfo {
int vertexID;
int distance;
};
struct FunctorTin {
template<typename T>
static bool operator()(const T& a, const T& b) {
return a.distance < b.distance;
}
};
template<typename T>
struct FunctorTout {
static bool operator()(const T& a, const T& b) {
return a.distance < b.distance;
}
};
template <typename T>
bool Func(const T& a, const T& b) {
return a.distance < b.distance;
}
bool FuncAuto(const auto& a, const auto& b) {
return a.distance < b.distance;
}
int main() {
std::vector<DijkstraInfo> vec;
vec.resize(10);
vec[0].distance = 3;
vec[2].distance = 2;
vec[3].distance = 8;
vec[5].distance = 2;
vec[7].distance = 6;
vec[9].distance = 5;
for (std::size_t i = 0; i < 10; i++) {
vec[i].vertexID = static_cast<int>(i);
std::println("id: {}, d: {}", vec[i].vertexID, vec[i].distance);
}
std::println("");
auto LambdaFunc = [](const auto& a, const auto& b) { return a.distance < b.distance; };
std::ranges::sort(vec, LambdaFunc);
std::ranges::sort(vec, FunctorTin());
std::ranges::sort(vec, FunctorTout<DijkstraInfo>());
std::ranges::sort(vec, Func<DijkstraInfo>);
std::ranges::sort(vec, FuncAuto<DijkstraInfo, DijkstraInfo>);
for (auto& m: vec) {
std::println("id: {}, d: {}", m.vertexID, m.distance);
}
return 0;
}

通过结构体包装, 延迟实例化. 如果我们将模板函数封装在一个结构体中, 编译器可以在调用时根据需要进行模板实例化.

10#

解答Lee的问题; 你应该如果解决?

Answer#

因为传了引用.