C++学习笔记

C++作为一门入门容易,精通难的语言,具有许多丰富多彩的用法,要想一口气吃成大胖子不太实际,随开本篇博客进行记录,长期更新,共勉!

目录

1.文件操作

2.命令行参数

3.C++中的lambda表达式

4.cstdio中的sprintf,sscanf字符串操作

5.操作文件fopen,fclose,fseek,ftell,fread,fwrite,fputs,fgets函数

 6.C++类中的typeid和dynamic_cast操作符

7.override和const关键字

8.在派生类中使用using baseName::memberfunction

9.数据成员指针

10.类的移动赋值,移动构造

11.模板函数中的模板类型变量的初始化

12.new操作符初始化类对象

13.auto和decltype函数

14.内部类

 15.关于类的一些注意事项

16.list库

17.typename关键字

18.模板函数编写时对C风格数组更为优秀的传入

19.constexpr说明符

20.使用using语法定义别名

21.dynamic_cast和static_cast进行类型转换


1.文件操作<fstream>

当然,更简单的方法是利用命令行中的重定向来进行文件的读入和读取,比如一个test.exe文件,我们可以让命令行转移到此根目录下,然后使用test.exe < in.txt > out.txt命令来重定向文件输入和文件输出,不过,这里将要介绍的是<fstream>这个头文件。

 

 

 

 

 

 

#include <fstream>

struct Data {
    int id;
    double value;
};

int main() {
    Data data = {1, 3.14};

    // 写入 data 到文件
    std::ofstream outfile("data.bin", std::ios::binary);
    if (outfile) {
        outfile.write(reinterpret_cast<const char*>(&data), sizeof(Data));
        outfile.close();
    }

    // 从文件读取 data
    Data readData;
    std::ifstream infile("data.bin", std::ios::binary);
    if (infile) {
        infile.read(reinterpret_cast<char*>(&readData), sizeof(Data));
        infile.close();
    }

    return 0;
}

 

#include <fstream>
#include <iostream>

int main() {
    // 使用 put() 写入文件
    std::ofstream outfile("example.txt");
    if (outfile) {
        outfile.put('A');
        outfile.close();
    }

    // 使用 get() 读取文件
    std::ifstream infile("example.txt");
    if (infile) {
        char c;
        infile.get(c); // 读取一个字符
        std::cout << "Read from file: " << c << std::endl;
        infile.close();
    }

    return 0;
}

#include <fstream>
#include <iostream>

int main() {
    std::fstream file("example.bin", std::ios::binary | std::ios::in | std::ios::out);

    // 写入数据
    if (file) {
        file.write("Hello, World!", 13);
    }

    // 移动到文件开始
    file.seekg(0);

    // 读取数据
    char buffer[14] = {};
    if (file) {
        file.read(buffer, 13);
        std::cout << "Read from file: " << buffer << std::endl;
    }

    file.close();
    return 0;
}

#include <fstream>
#include <iostream>

int main() {
    std::fstream file("example.bin", std::ios::binary | std::ios::in | std::ios::out | std::ios::app);

    // 写入数据并获取写入位置
    file.write("Hello, World!", 13);
    std::streampos writePos = file.tellp();
    std::cout << "Write position: " << writePos << std::endl;

    // 重置位置,并读取数据
    file.seekg(0);
    char buffer[14] = {};
    file.read(buffer, 13);
    std::streampos readPos = file.tellg();
    std::cout << "Read position: " << readPos << std::endl;

    file.close();
    return 0;
}

注意

对于一个 fstream 对象,tellg()tellp() 得到的指针位置并不总是一样的。这是因为 fstream 同时继承自 ifstreamofstream,分别管理输入和输出流。在某些情况下,输入指针(get pointer)和输出指针(put pointer)可能会分别移动,导致它们指向的位置不同。

  • 当你使用 fstream 以读写模式打开文件时,如果仅进行写操作(如使用 << 操作符或 write() 方法),则只会更新输出指针(put pointer)的位置。相应地,使用读操作(如使用 >> 操作符或 read() 方法)会更新输入指针(get pointer)的位置。

  • 如果在写入操作后立即进行读取操作,或者在读取操作后立即写入,可能需要手动同步两个指针的位置。例如,在写入后想要读取相同的数据,可能需要使用 seekg() 将读取指针移动到写入的起始位置。同样地,如果在读取某些数据后想要在相同的位置开始写入,可能需要使用 seekp() 来更新写入指针的位置。

  • 在某些实现中,进行输出操作(如写入)可能会自动移动输入指针(get pointer)到文件末尾,反之亦然。但这种行为并不是所有环境下都一致的,因此依赖于此行为的代码可能不具有移植性.

#include <fstream>
#include <iostream>

int main() {
    std::fstream file("example.txt", std::ios::binary | std::ios::in | std::ios::out | std::ios::trunc);

    if (!file) {
        std::cerr << "File could not be opened." << std::endl;
        return 1;
    }

    // 写入数据
    file << "Hello, world!";
    std::cout << "After writing, put pointer is at: " << file.tellp() << std::endl;

    // 读取数据之前,需要同步get和put指针
    file.seekg(0);
    std::string content;
    file >> content;
    std::cout << "After reading, get pointer is at: " << file.tellg() << std::endl;

    file.close();
    return 0;
}

当你观察到 tellp()tellg() 返回相同位置的情况,这通常是因为在某些标准库实现中,对于 fstream 对象,即使是分别用于输入和输出的指针,它们在某些操作之后可能会被同步更新,特别是在对文件进行写入后立即查询位置、或在读取操作后进行位置查询的场景中。

这种行为部分取决于具体的标准库实现和文件模式。当 fstream 被以读写模式打开时,某些实现可能会选择在每次写操作后同步两个指针的位置,以简化文件指针管理逻辑。这意味着,当你写入数据后(使用 << 运算符或其他写入方法),并查询写入指针 (tellp()) 和读取指针 (tellg()) 的位置,你可能会看到它们指向同一个位置,即最后一次操作的位置。

此外,标准库的实现可能会在你执行读取操作前后自动调整这些指针,以确保数据的一致性和访问的有效性。这种自动同步的行为有时候可以方便开发者,但也可能导致一些混淆,尤其是当你期望这两个指针能够独立移动时。

在你的示例代码中,你首先进行写入操作,然后立即查询了 tellp()tellg() 的值,此时它们显示为相同。这可能是因为在你的标准库实现中,写操作后自动同步了输出和输入指针。随后,你使用 seekg() 调整了读取位置,并进行了读取操作。如果在这之后 tellp()tellg() 依旧相同,这也可能是因为你的读取操作触发了某种同步机制。

需要注意的是,这种行为并不是所有编译器或标准库实现都会这样处理。因此,编写依赖于这种特定行为的代码时要小心,因为它可能不具备跨编译器的可移植性。

2.命令行参数

3.C++中的lambda表达式

  • auto f = [&] (double x) { return x * x; };

    • & 表示 Lambda 表达式可以捕获外部作用域中的所有变量,并通过引用的方式访问它们。
    • (double x) 是参数列表,表示这个 Lambda 接受一个 double 类型的参数 x
    • { return x * x; } 是函数体,定义了函数的行为,即返回参数 x 的平方。
  • auto g = [=] (double x) { return std::sin(x); };

    • = 表示 Lambda 表达式可以捕获外部作用域中的所有变量,并通过值的方式访问它们。这意味着在 Lambda 表达式内部,你可以访问这些变量的拷贝,但不能修改它们的值。
    • (double x) 是参数列表,与 f 相同。
    • { return std::sin(x); } 是函数体,定义了函数的行为,即返回参数 x 的正弦值。

当然,我们也可以讲lambda函数作为返回值来耦合一些表达式:

#include <iostream>
#include <iomanip>
#include <cmath>

constexpr double DX = 1e-6;
// 在此处补充你的代码
/*template<class T>
auto d(T f){
    auto df=[f](double x){
        return (f(x+DX)-f(x))/DX;
    };
    return df;
}*/
auto d = [](auto func) {
    return [func](double x) {
        return (func(x + DX) - func(x)) / DX;
    };
};

int main() {
    auto f = [&] (double x) { return x * x; };
    auto g = [=] (double x) { return std::sin(x); };

    double x;
    std::cout << std::fixed << std::setprecision(2);
    while (std::cin >> x) {
        std::cout << d(f)(x) << ' ';
        std::cout << d(g)(x) << std::endl;
    }
}
4.cstdio中的sprintf,sscanf字符串操作

 

5.操作文件fopen,fclose,fseek,ftell,fread,fwrite,fputs,fgets函数

 

 

 

 

 

 

 6.C++类中的typeid和dynamic_cast操作符

class Base {
public:
    virtual void print() const { std::cout << "This is Base class." << std::endl; }
    virtual ~Base() {}
};

class Derived : public Base {
public:
    void print() const override { std::cout << "This is Derived class." << std::endl; }
    void derivedFunction() const { std::cout << "Function specific to Derived class." << std::endl; }
};

int main() {
    Base* basePtr = new Derived();
    basePtr->print(); // 正确调用Derived的print方法

    // 尝试将Base类型指针转换为Derived类型指针
    Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
    if (derivedPtr != nullptr) {
        derivedPtr->print(); // 如果转换成功,调用Derived的print方法
        derivedPtr->derivedFunction(); // 调用Derived特有的函数
    } else {
        std::cout << "Conversion failed." << std::endl;
    }

    delete basePtr; // 清理资源
    return 0;
}

在这个例子中,basePtr指向一个Derived类的实例。通过使用dynamic_cast<Derived*>(basePtr)尝试将basePtr转换为Derived*类型。如果转换成功,即basePtr实际指向一个Derived类的对象,那么derivedPtr将不是nullptr,可以安全地调用Derived类的成员。如果转换失败,则derivedPtr将是nullptr,在实际应用中需要对这种情况进行处理。

dynamic_cast是RTTI机制的一部分,虽然它使得类型转换更加安全,但也会带来一定的性能开销,因为类型检查是在运行时进行的。因此,在设计程序时,应当权衡其使用。

7.override和const关键字

overrideconst关键字在C++中具有重要的作用,尤其是在类的成员函数中。

override

override关键字用于显式地指示一个成员函数覆盖了基类中的虚函数。这有助于编译器检查函数的重写(override)是否符合预期,即是否有一个同名的虚函数存在于基类中,具有相同的参数和相兼容的返回类型。使用override可以避免由于函数签名中的细微差异(例如参数类型的不匹配)导致的意外重载(overload)而非预期的重写。

使用override的好处:

  • 提高代码的可读性和可维护性。
  • 编译时检查,避免重写虚函数时的错误。
class Base {
public:
    virtual void someFunction() const;
};

class Derived : public Base {
public:
    void someFunction() const override; // 明确表示该函数意图覆盖基类的虚函数
};

const

在成员函数后面使用const关键字表示该成员函数不会修改类的任何成员变量(除了那些用mutable关键字声明的)。这种成员函数可以被称为“常量成员函数”。在const对象或const对象引用/指针上,只能调用常量成员函数。

使用const的好处:

  • 增加方法的安全性,防止不小心修改对象状态。
  • 允许在const对象上调用方法。
class MyClass {
public:
    void nonConstFunc() {} // 非const函数
    void constFunc() const {} // const函数
};

const MyClass obj;
obj.constFunc(); // 正确
// obj.nonConstFunc(); // 错误,因为obj是const对象

const用于函数重载

const还可以用于成员函数的重载。即可以有两个相同名称和参数列表的函数,其中一个是const版本,另一个是非const版本。这允许类根据对象是否为const来选择合适的函数版本。

class MyClass {
public:
    void func() { std::cout << "non-const func\n"; }
    void func() const { std::cout << "const func\n"; }
};

MyClass obj;
const MyClass cObj;

obj.func(); // 调用非const版本
cObj.func(); // 调用const版本

const类对象

使用const声明的类对象确实只能调用被标记为const的成员函数。这是因为const对象保证不会被修改,因此只能调用那些不会修改对象状态的成员函数。

总的来说,overrideconst都是C++中增加代码安全性、清晰性和维护性的重要关键字。

是的,普通成员函数可以访问const成员函数。在C++中,const成员函数承诺不会修改对象的状态,这意味着它们不会修改对象的任何非静态成员变量(除非这些成员变量被声明为mutable)。因此,它们可以在类的任何成员函数中被调用,无论这些成员函数是否也是const。

这里的关键是理解const成员函数的约束(不修改对象状态)允许它们在更广泛的上下文中安全使用,包括那些可能会修改对象状态的普通(非const)成员函数中。反过来,普通成员函数可能会改变对象状态,所以它们不能被const成员函数直接调用,因为这将违反const成员函数的约定。

8.在派生类中使用using baseName::memberfunction

在C++中,使用using语句的这种形式是为了解决名字隐藏(name hiding)问题,特别是在继承中。当一个派生类从两个或更多的基类继承时,如果基类中有同名的成员函数,派生类会隐藏这些同名的基类成员函数,即使它们的参数列表不同。这意味着,如果不采取措施,派生类对象将无法直接访问这些被隐藏的基类成员函数。

使用using BaseClass::memberFunction;语句,可以将基类中的一个或多个被隐藏的同名成员函数“提升”到派生类的作用域中。这样做的结果是,这些函数就像是在派生类中声明的一样,可以被正常调用,从而实现函数的重载解析。

#include <iostream>

struct BaseInt {
private:
	int value;
	
public:
	void set(int x) { value = x; }
	int getInt() { return value; }
};

struct BaseDouble {
private:
	double value;
	
public:
	void set(double x) { value = x; }
	double getDouble() { return value; }
};

class Derived : public BaseInt, public BaseDouble {
	
// 在此处补充你的代码
public:
	/*此处是方法一
	template<class T>
	void set(T a){
		if(typeid(T)==typeid(int)){
			BaseInt::set(a);
		}
		else{
			BaseDouble::set(a);
		}
	}
	*/
	using BaseInt::set;
	using BaseDouble::set;
	
};

int main() {
	Derived obj;
	int i;
	double d;
	while (std::cin >> i >> d) {
		obj.set(i), obj.set(d);
		std::cout << obj.getInt() << ' '
		<< obj.getDouble() << std::endl;
	}
}
9.数据成员指针

数据成员指针是指向数据成员的指针。更底层地讲,它持有某一数据成员相对于整个对象的地址偏移量

比如,对于结构体struct S { int a, b; };,指针值 &S::b 就代表了成员 b 在整个 S 中的偏移量。

类似指针变量,也存在存储数据成员指针的变量。声明这样的变量的方法是 T S::*mem 初始化器,其中 T 是成员的类型,S 是类(结构体)名,mem 是变量名。比如,初始化指针变量 ptr 持有成员 S::a 偏移量的初始化声明写成:int S::*ptr = &S::a;

可以通过 .* 运算符或 ->* 运算符使用成员指针。比如对于对象 S obj;obj.*ptr 即可访问到 obj.a 成员。

#include <iostream>

struct Point {
	int x, y, z;
	
// 在此处补充你的代码
	Point():x(0),y(0),z(0) {}
	void translate(int Point::*d,int v){
		//(*this).*d+=v;
		this->*d+=v;
	}
	//
	void print() const {
		std::cout << "(" << x << ", " << y << ", " << z << ")\n";
	}
};

int main() {
	Point p{};
	
	char c;
	int x;
	while (std::cin >> c >> x) {
		int Point::*dir = nullptr;
		switch (c) {
			case 'x': dir = &Point::x; break;
			case 'y': dir = &Point::y; break;
			case 'z': dir = &Point::z; break;
		}
		p.translate(dir, x);
		p.print();
	}
}
10.类的移动赋值,移动构造

当我们用同类型的类类型变量进行赋值时,一般会调用复制赋值运算符重载,一般声明为T& operator=(const T& t),其中T为该类类型。

现在有一个需要深复制的类String,当我们用一个String类型的变量赋值给一个String类对象时,按照深复制的操作,需要额外分配内存,再将该值复制过去。

例如,我们执行String s; s = "abc";这段代码时,会首先开辟一片内存,将"abc"这一字符串字面量转换构造为String类型。接着,再在s中开辟一片内存空间,将"abc"对应的那个String类型变量的内容深复制过去。

我们会发现,这样操作会导致额外分配内存空间操作,造成浪费。因为给"abc"这一字面量分配的那个String类型变量会在完成赋值操作后析构从而被释放,这篇内存空间也不会再使用。而我们又可以发现,s原先所指向的那一片内存空间所对应的内容将在赋值完成后不再被使用,所以我们可以通过“交换”s"abc"对应的内存空间,在少分配1次新的内存空间的情况下同时完成“赋值”和“释放”原有空间的作用,就好像将原来的内容”移动“到了新的内存空间中。

我们分析一下何时才能使用上述操作,可以发现需要这一条件:赋值运算符右侧的变量将在赋值操作完成后立刻被释放,即赋值运算符右侧为右值表达式(字面量、a+b等等)。而当右侧为左值表达式时,则不能采取这一交换手段。

因此,我们可以利用”移动赋值运算符“重载实现这一”交换“手段。

#include <cassert>
#include <iostream>
#include <cstring>
using std::cout, std::endl;
class String{
public:
    int len;
    char* str;
    String(const char* x){
        len = strlen(x);
        str = new char[len + 1];
        strcpy(str, x);
    }
String& operator=(String&& b){
		if(str) delete[] str;
		len=b.len;
		str=b.str;
		b.str=nullptr;
		b.len=0;
		return *this;
	}
	String& operator=(const String& b){
		if(this==&b) return *this;
		len=b.len;
		if(str) {delete[] str; str=new char[b.len+1];}
		else str=new char[b.len+1];
		strcpy(str,b.str);
		return *this;
	}
//
    ~String(){
        delete[] str;
    }    
};

int newCount{0};
int nowUsed{0};
//下列代码是内存分配计数器的实现,不用管
void* operator new[](std::size_t size) {
    if (void* ptr = std::malloc(size)) {
        newCount++;
        nowUsed++;
        return ptr;
    }
    throw std::bad_alloc();
}
void operator delete[](void* ptr) noexcept {
    std::free(ptr);
    nowUsed--;
}

int main(){
    {
    String a("Hello");
    String b("World");
    String c("");
    c = String("Hi");
    cout << c.str << endl;
    c = a;
    cout << c.str << endl;
    cout << a.str << endl;
    (c = a) = b;
    cout << c.str << endl;
    cout << b.str << endl;
    (c = "Hi") = a;
    cout << c.str << endl;
    }
    // assert: 若条件不满足,则运行时错误
    // 整份代码应当分配 9 次内存,且不应内存泄漏
    assert(newCount == 9);
    assert(nowUsed == 0);
    return 0;

在有些时候,我们会定义一些临时变量,其中的值只作暂时储存用,而无需长时间保留。例如在如下这段代码:

void swap(T& a, T& b) {
    T tmp = a;
    a = b;
    b = tmp;
}

它实现了两个类类型变量的交换,但其中调用了1次复制构造函数和2次复制赋值运算符重载。但我们发现,这里用作传递的三次"="的变量仅作为临时储存使用,无需进行真正的"复制",也即无需将初始化或赋值操作的右侧操作数的值保留下来,即可以将值“移动”过去。

但这里的 abtmp 都是左值表达式,无法调用移动构造函数或移动赋值运算符重载。在这种情况下,我们可以通过 std::move 函数强制将左值转换为右值,从而调用移动构造函数和移动赋值运算符重载。(注意:这种操作较为危险,需要想清楚这一操作是否真的可以被“移动”)

#include <iostream>
#include <cassert>
using std::cout, std::endl;
class A {
private:
	int val;
public:
	static int constructorCount;
	A(int x) {
		constructorCount++;
		val = x;
	}
	// 本题不应调用复制构造函数和复制赋值运算符
	A(A& x) = delete;
	A& operator=(A& x) = delete;
	A(A&& x) {
		val = x.val;
		x.val = 0; // 将x.val赋0模拟"移动"语义
	}
	A& operator=(A&& x) {
		val = x.val;
		x.val = 0; // 将x.val赋0模拟"移动"语义
		return *this;
	}
	int getVal() const {
		return val;
	}
};
int A::constructorCount = 0;

// 在此处补充你的代码
template<class T>
void swap(T& a,T& b){
	T tmp=std::move(a);
	a=std::move(b);
	b=std::move(tmp);
}

int main(){
	A a(1), b(2);
	swap(a, b);
	cout << a.getVal() << " " << b.getVal() << endl;
	// 整份代码不应当再额外调用构造函数
	assert(A::constructorCount == 2);
}
11.模板函数中的模板类型变量的初始化

12.new操作符初始化类对象

 

13.auto和decltype函数

 

 

 

14.内部类

#include <iostream>
using namespace std;

class Outer {
public:
    Outer() : x(0) {}

    class Inner {
    public:
        void display() {
            // 访问外部类的私有成员
            cout << "外部类的私有成员 x = " << outer.x << endl;
        }
    };

    // 外部类的成员函数,用于创建和使用内部类对象
    void createInner() {
        Inner inner;
        inner.display();
    }

private:
    int x;
};

int main() {
    Outer outer;
    outer.createInner();  // 通过外部类的对象来使用内部类
    return 0;
}

这个示例中,Inner是定义在Outer类内部的一个内部类。Inner类有一个display成员函数,可以访问Outer类的私有成员x。然后,Outer类有一个createInner成员函数,创建了一个Inner类的对象并调用了其display方法。这种方式允许我们将与Outer类紧密相关但又希望在逻辑上分隔的代码组织到一个内部的命名空间中,提高了代码的可读性和可维护性。

 

 

 

 

 

 15.关于类的一些注意事项

 

 

16.list库

list可以通过多种方式进行初始化

#include <list>
#include <iostream>

int main() {
    std::list<int> list1; // 空列表
    std::list<int> list2(4, 100); // 包含4个值为100的元素
    std::list<int> list3(list2.begin(), list2.end()); // 通过另一个list初始化
    std::list<int> list4 = {1, 2, 3, 4, 5}; // 列表初始化器

    // 输出list4
    for(int n : list4) {
        std::cout << n << ' ';
    }
    std::cout << '\n';

    return 0;
}
  • push_back(e):在列表末尾添加一个元素e。
  • push_front(e):在列表开头添加一个元素e。
  • pop_back():删除列表末尾的元素。
  • pop_front():删除列表开头的元素。
  • insert(pos, e):在指定位置pos前插入一个元素e,并返回新插入元素的迭代器。
  • erase(pos):删除指定位置pos的元素,并返回下一个元素的迭代器。
  • remove(val):删除列表中所有值为val的元素。
  • size():返回列表中的元素个数。
  • clear():清空列表中的所有元素。
  • sort():对列表中的元素进行排序。
  • reverse():反转列表中的元素顺序。
#include <list>
#include <iostream>

int main() {
    std::list<int> myList = {2, 4, 6, 8};

    myList.push_back(10); // 在末尾添加10
    myList.push_front(0); // 在开头添加0
    myList.insert(++myList.begin(), 3); // 在第二个位置插入3

    myList.erase(--myList.end()); // 删除最后一个元素

    // 输出列表
    for(int n : myList) {
        std::cout << n << ' ';
    }
    std::cout << '\n';

    // 反转并输出列表
    myList.reverse();
    for(int n : myList) {
        std::cout << n << ' ';
    }
    std::cout << '\n';

    return 0;
}

 

 

 

17.typename关键字

typename 关键字在 C++ 中扮演了非常重要的角色,尤其是在模板编程中。它主要用于两个场景:一是在声明模板参数时,二是在指明依赖类型名称。让我们逐一深入了解这两个用途及其重要性。

1. 声明模板参数

在定义模板类或函数时,typename 可用来声明类型模板参数。这里,它与关键字 class 可以互换使用,没有区别。它们都指示编译器,紧随其后的模板参数是一个类型。

template<typename T>
class MyClass {
    // 使用 T
};

template<class T>
void myFunction(T param) {
    // 使用 T
}

在这个用法中,typenameclass 完全等价,你可以根据个人喜好或代码风格指南选择使用哪一个。

2. 指明依赖类型名称

更复杂且更为关键的用法是在模板定义中指定依赖于模板参数的类型名。当编译器解析模板代码时,它可能无法立即知道某个特定名称是否表示一个类型,特别是在涉及模板参数的成员时。typename 在这里的作用是明确告诉编译器,特定的名称是一个类型名称。

这种情况通常出现在模板代码尝试访问某个类型的嵌套类型时:

template<typename Container>
void printFirstElement(const Container& c) {
    if (!c.empty()) {
        typename Container::const_iterator it = c.begin(); // 明确 const_iterator 是一个类型
        std::cout << *it << std::endl;
    }
}

在上述代码中,如果没有 typename,编译器可能无法确定 Container::const_iterator 是否是一个类型,这可能导致编译错误。通过在前面加上 typename,我们明确指出 Container::const_iterator 是一个类型。

为什么需要 typename

考虑到模板的通用性,编译器在处理模板代码时不总是能够知道模板参数的具体类型,尤其是当涉及到从模板参数类型派生的类型时。使用 typename 可以避免这种不确定性,确保代码的清晰和正确性。

注意事项

  • 在使用 typename 指明依赖类型时,不能用于基本类型(如 intdouble 等)或模板参数本身,因为这些场景下编译器已经知道你指的是一个类型。
  • C++11 引入了 auto 关键字,它在很多情况下可以避免使用 typename,尤其是在变量声明中自动推断类型时。然而,在表示嵌套依赖类型时,typename 仍然是必须的。
  • 从 C++20 开始,使用 typename 在模板参数声明中区分类模板实例化变得更加灵活,尤其是在使用模板模板参数时。

理解 typename 的这些用法对于编写正确、健壯和可读的模板代码至关重要。随着你对模板编程的深入,这个关键字的重要性将变得更加明显。

 

 

18.模板函数编写时对C风格数组更为优秀的传入
template<typename T, size_t N, typename Function>
T sumIf(T (&arr)[N], Function f) {
    T sum = 0;
    for (size_t i = 0; i < N; ++i) {
        if (f(arr[i])) sum += arr[i];
    }
    return sum;
}

在 C++ 中,T (&arr)[N] 表示一个对数组的引用。这里,T 是数组元素的类型,N 是数组的大小。让我们逐部分解析这个表达式:

  1. T:这是数组元素的类型。例如,如果有一个整型数组,T 就是 int
  2. arr:这是数组的名称,在这个上下文中,arr 是一个引用变量的名称,用于指向传入的数组。
  3. []:这个部分指示 arr 是一个数组。
  4. N:这表示数组的大小,是编译时常量。这个大小在数组作为参数传递给函数时必须已知。

将这些结合起来,T (&arr)[N] 可以理解为“一个大小为 N 的数组的引用,该数组的元素类型为 T”。使用这种语法可以避免数组在作为参数传递时退化为指针,同时允许函数模板推断出数组的大小。

这种语法在模板函数中尤为有用,因为它允许函数处理大小在编译时已知的数组,同时避免了数组到指针的隐式转换,保留了数组的长度信息,使得在函数内部可以安全地对数组进行操作。

允许函数模板推断出数组的大小是一项非常有用的特性。这是通过在函数模板参数中使用数组引用语法来实现的。当你这样做时,编译器能够自动推断出数组的大小,这使得模板函数可以适用于不同大小的数组而无需在函数调用时显式指定数组的大小。

这种方式的一个主要优点是它提高了代码的灵活性和安全性。因为数组的大小在编译时已知,编译器可以进行更多的检查,减少运行时错误的可能性。同时,它也避免了数组到指针的隐式转换,这种转换通常会丢失关于数组大小的信息。

看一个简单的例子,说明如何通过模板参数自动推断数组的大小:

#include <iostream>

template<typename T, size_t N>
void printArray(T (&arr)[N]) {
    for (size_t i = 0; i < N; ++i) {
        std::cout << arr[i] << ' ';
    }
    std::cout << '\n';
}

int main() {
    int myArray[] = {1, 2, 3, 4, 5};
    printArray(myArray); // 在这里不需要指定数组大小
    return 0;
}

在这个例子中,printArray函数模板有两个参数:T是数组元素的类型,N是数组的大小。当printArray(myArray)被调用时,编译器自动推断出myArray的类型是int,大小是5,因此模板实例化为printArray<int, 5>。这个特性让模板函数更加灵活和安全,尤其是在处理数组时。

注意,这样的传入使得可以在函数体内修改这个数组,所以可以加个const来防止修改。

#include <iostream>

template<typename T, size_t N>
void printArray(const T (&arr)[N]) {
	for (size_t i = 0; i < N; ++i) {
		std::cout<<arr[i]<<std::endl;
	}
	std::cout << '\n';
}

int main() {
	int myArray[] = {1, 2, 3, 4, 5};
	printArray(myArray); // 在这里不需要指定数组大小
	return 0;
}
19.constexpr说明符

constexpr是"constant expression"的缩写,它是C++11引入的关键字,用于声明一个常量表达式。在C++中,常量表达式是在编译时就能够得到计算结果的表达式。constexpr关键字可以用于变量、函数以及构造函数,其作用是告诉编译器在编译时就能够计算这些表达式的值。

对于变量,constexpr声明的变量必须在编译时就能够得到值,并且其值在运行时是不可改变的。这样的变量通常被称为"编译期常量"。

对于函数,constexpr声明的函数可以在编译时被计算,从而可以在编译时进行优化。这种函数通常用于执行简单的计算,而不需要在运行时进行计算。

对于构造函数,constexpr可以用于要求编译时执行的构造函数。这样的构造函数可以在编译时被调用,从而允许在编译期间进行一些初始化操作。

总的来说,constexpr关键字的引入使得C++在编译时能够进行更多的优化和检查,从而提高了程序的性能和安全性。

 

20.使用using语法定义别名

这行代码使用了C++中的using关键字来定义一个别名。具体来说,using AlsoInt = int;这行代码将AlsoInt定义为int类型的别名。

在C++11及以后的标准中,using关键字可以用来定义别名,其语法为using 新名字 = 原类型;。这种方式与传统的typedef语法类似,但更加直观和灵活。

因此,using AlsoInt = int;这行代码实际上是在定义一个名为AlsoInt的新类型,它是int类型的别名。这意味着在使用AlsoInt时,就等同于使用int类型,两者可以互换使用。这种别名的定义可以使代码更易读,同时也可以提高代码的可维护性。

21.dynamic_cast和static_cast进行类型转换

 

 dynamic_cast 示例

#include <iostream>

class Base {
public:
    virtual void print() const { std::cout << "Base class" << std::endl; }
    virtual ~Base() {} // 虚析构函数以启用多态
};

class Derived : public Base {
public:
    void print() const override { std::cout << "Derived class" << std::endl; }
    void specificFunction() const { std::cout << "Derived specific function" << std::endl; }
};

int main() {
    Base* basePtr = new Derived();
    basePtr->print(); // 输出: Derived class
    
    // 尝试将 basePtr 转换为 Derived 类型的指针
    Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
    if (derivedPtr) {
        derivedPtr->specificFunction(); // 输出: Derived specific function
    } else {
        std::cout << "Conversion failed" << std::endl;
    }

    delete basePtr; // 清理资源
    return 0;
}

 在这个示例中,dynamic_cast用于将基类指针basePtr转换为派生类指针derivedPtr,因为实际指向的对象是Derived类的实例,转换成功,并能安全调用派生类特有的方法specificFunction

static_cast示例

#include <iostream>

class Base {
public:
    void baseFunction() const { std::cout << "Base function" << std::endl; }
};

class Derived : public Base {
public:
    void derivedFunction() const { std::cout << "Derived function" << std::endl; }
};

int main() {
    Derived derivedObj;
    Base* basePtr = &derivedObj; // 派生类对象的地址赋给基类指针
    basePtr->baseFunction(); // 正确,调用基类的方法

    // 使用 static_cast 将基类指针转换为派生类指针
    Derived* derivedPtr = static_cast<Derived*>(basePtr);
    derivedPtr->derivedFunction(); // 正确,调用派生类的方法

    return 0;
}

static_cast的示例中,我们首先将派生类对象的地址赋给基类指针basePtr。然后,使用static_castbasePtr转换回派生类指针derivedPtr,以调用派生类特有的方法derivedFunction。这种转换是安全的,因为我们确信basePtr实际上指向一个Derived类型的对象。

这两个示例展示了dynamic_caststatic_cast在类层次中类型转换的典型用法。选择哪种转换方式取决于你的具体需求和转换的上下文。

22.使用reinterpret_cast和const_cast进行类型转换

reinterpret_cast

reinterpret_cast用于类型之间的低级转换,可以将任何指针类型转换成任何其他指针类型(即使它们之间没有任何关系)。同样,它也可以用于指针与足够大的整数类型之间的转换。使用reinterpret_cast的代码应谨慎编写,因为这种转换的安全性由程序员负责。

const_cast

const_cast主要用于修改类型的const或volatile属性,尤其是去除const属性。它不能改变表达式的类型,但可以去除或添加const属性。请注意,去除一个对象的const属性并对其进行修改,如果该对象本身是const定义的,则是未定义行为。

23.explicit的进一步补充

在 C++11 及之后的版本中,当一个 explicit 转换操作符被用在逻辑上下文中,例如 ifwhilefor 条件或 !&&|| 等操作符中时,编译器会自动将其视为显式转换。即使 explicit 关键字存在,编译器也会调用 explicit 转换操作符来评估条件。

23.typename进一步补充

在C++中,typename 关键字用于告诉编译器接下来的名称应被解释为一个类型。在模板编程中,特别是在处理模板参数依赖的类型时,typename 是必需的。这种情况通常出现在你要从模板类型参数中引用嵌套的依赖类型时。

例如,假设 Container 是一个模板参数,那么 Container::iterator 是一个依赖于 Container 类型的嵌套类型。在这种情况下,编译器不知道 Container::iterator 是一个类型还是一个成员变量,因为具体的类型直到模板实例化之前都是未知的。通过在前面加上 typename,你告诉编译器 Container::iterator 是一个类型,从而正确解析。

如果不加 typename,编译器在解析代码时可能会产生错误,因为它可能不会将 Container::iterator 识别为类型。这会导致编译失败,通常会提示类型或声明相关的错误。

24.sort推导特性

在C++中,当你使用 std::sort 并传递 std::greater 作为比较函数时,通常需要指定比较对象的类型,如 std::greater<Point>。但在你的代码示例中,因为你已经为 Point 类定义了 <> 操作符,编译器能够自动推导出 std::greater 应用于哪种类型。这是 C++17 标准引入的特性,称为模板参数推导(template argument deduction),它允许编译器根据传入函数的参数自动推导模板参数类型。

因此,当你调用 std::sort(points, points + n, std::greater()) 时,编译器能够理解 std::greater 应该用于 Point 类型,省略了模板参数 <Point>

  • 15
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值