c++--速总
参考博客
修饰符
const
在 C++ 中,const
关键字用于定义不可修改的变量、指针、函数参数和返回值等。它可以增强代码的安全性和可读性,防止意外修改数据。
1. 常量变量
使用 const
定义的变量是不可更改的常量。一旦赋值,就不能再修改。
const int x = 10;
// x = 20; // 错误,x 是一个常量,不能修改
2. 常量指针
指针可以与 const
结合,来控制指针或指针指向的数据是否可以被修改:
-
指向常量的指针
指针指向的值不能改变,但指针本身可以改变。const int* ptr = &x; // 指针指向一个常量 // *ptr = 20; // 错误,不能修改指针指向的值 int y = 15; ptr = &y; // 可以修改指针本身指向不同的变量
-
常量指针
指针本身不能改变,但指针指向的值可以改变。int* const ptr = &x; // 常量指针 *ptr = 20; // 可以修改指针指向的值 // ptr = &y; // 错误,不能修改指针本身
-
指向常量的常量指针
指针本身和指针指向的值都不能改变。const int* const ptr = &x; // 指向常量的常量指针 // *ptr = 20; // 错误,不能修改指针指向的值 // ptr = &y; // 错误,不能修改指针本身
3. 常量成员函数
在类中,使用 const
修饰成员函数表示该函数不会修改类的成员变量。常量成员函数只能调用其他常量成员函数。
class MyClass {
public:
int getValue() const { // 常量成员函数
return value;
}
private:
int value = 10;
};
命名空间
命名空间(namespace)是一种用于组织代码和避免命名冲突的机制。
命名空间为标识符(如变量名、函数名、类名等)提供了一个上下文,以便相同名字的标识符可以在不同的命名空间中共存,而不会产生冲突。
在 C++ 中,命名空间(namespace)是一种用于组织代码和避免命名冲突的机制。命名空间为标识符(如变量名、函数名、类名等)提供了一个上下文,以便相同名字的标识符可以在不同的命名空间中共存,而不会产生冲突。
为什么需要命名空间?
随着代码库的增长和多个库的集成,可能会出现命名冲突的情况,比如不同的库中可能会有相同名字的函数、类或者变量。如果没有命名空间的存在,这些名字就会发生冲突,导致编译错误。命名空间可以解决这个问题。
1. 定义命名空间
命名空间通过 namespace
关键字定义,它可以包含变量、函数、类、结构体、枚举等。
示例:
namespace MyNamespace {
int value = 42;
void display() {
std::cout << "Value: " << value << std::endl;
}
}
在这个例子中,MyNamespace
是一个命名空间,包含了一个整数变量 value
和一个函数 display()
。如果要访问这些变量和函数,就必须通过命名空间的名称来引用。
2. 使用命名空间中的内容
你可以使用命名空间的成员通过以下几种方式:
2.1 使用命名空间前缀
要使用命名空间中的变量或函数,你可以在名称前加上命名空间的名称作为前缀。
示例:
int main() {
MyNamespace::display(); // 使用命名空间前缀
std::cout << MyNamespace::value << std::endl;
}
2.2 使用 using
声明
通过 using
关键字,你可以将命名空间中的特定成员引入当前作用域,从而不需要每次都使用命名空间前缀。
示例:
int main() {
using MyNamespace::display;
display(); // 直接使用函数,不需要前缀
}
2.3 使用 using namespace
引入整个命名空间
你还可以通过 using namespace
将整个命名空间引入到当前作用域,从而无需使用前缀来访问命名空间中的所有成员。
示例:
int main() {
using namespace MyNamespace;
display(); // 不需要前缀
std::cout << value << std::endl;
}
注意:
using namespace
可能会引发命名冲突,尤其是在大范围中使用多个库时。因此,最好在局部作用域中使用,或者避免在全局范围内大量引入命名空间。
3. 标准命名空间 std
C++ 标准库中的所有库函数、类和对象都被定义在 std
命名空间中。因此,使用标准库的内容时,通常需要使用 std::
前缀,或者通过 using namespace std;
引入。
示例:
#include <iostream>
int main() {
std::cout << "Hello, World!" << std::endl; // 使用 std 命名空间
}
或者:
#include <iostream>
using namespace std;
int main() {
cout << "Hello, World!" << endl; // 不需要 std 前缀
}
使用案例:
有时,局部作用域中会有与全局作用域同名的变量、函数或类。在这种情况下,使用 :: 可以访问全局作用域中的符号。
例如:
int value = 10; // 全局变量
void func() {
int value = 20; // 局部变量
std::cout << value << std::endl; // 输出 20
std::cout << ::value << std::endl; // 使用 :: 访问全局变量,输出 10
}
在 func() 中,value 是局部变量,而 ::value 访问的是全局变量。
当你使用 C++ 标准库或者用户定义的命名空间中的某些符号时,你需要用 :: 来指定该符号属于哪个命名空间。例如:
std::string name = "Alice"; // 使用 std 命名空间中的 string 类
使用 std:: 明确指出 string 是标准库命名空间中的类,而不是全局作用域或其他命名空间中的同名符号。
面向对象
在 C++ 中,类(class) 是面向对象编程的核心概念之一。类是一种用户定义的类型,它封装了数据(成员变量)和行为(成员函数),并提供了对这些数据和行为的访问控制。
类的基本概念
- 类的声明:类是用户定义的类型,它定义了对象的属性(成员变量)和行为(成员函数)。
- 对象:类的实例化对象代表类的具体实现,每个对象都有自己的成员变量副本。
- 封装:类通过将数据和函数封装在一起来实现封装性。类可以控制哪些数据对外部可见(通过访问控制修饰符)。
- 继承:类可以通过继承其他类,复用已有类的功能,同时可以添加新的功能或重写已有功能。
- 多态性:通过基类指针或引用,动态调用不同派生类的实现,利用虚函数实现运行时多态。
类的定义与对象的使用
#include <iostream>
#include <string>
class Person {
private:
// 私有成员变量
std::string name;
int age;
public:
// 构造函数
Person(std::string n, int a) : name(n), age(a) {}
// 成员函数
void introduce() const {
std::cout << "Hello, my name is " << name << " and I am " << age << " years old." << std::endl;
}
// 获取年龄(只读的成员函数)
int getAge() const {
return age;
}
// 修改年龄
void setAge(int a) {
age = a;
}
};
int main() {
// 创建对象
Person person1("Alice", 30);
// 使用对象的成员函数
person1.introduce(); // 输出: Hello, my name is Alice and I am 30 years old.
person1.setAge(31); // 修改年龄
std::cout << "New age: " << person1.getAge() << std::endl; // 输出: New age: 31
return 0;
}
构造函数与析构函数
- 构造函数(Constructor):构造函数是一个与类名相同的特殊函数,用于在创建对象时初始化成员变量。它可以有参数或没有参数。
- 析构函数(Destructor):析构函数是一个用于在对象生命周期结束时清理资源的特殊函数。它的名字是类名前加上
~
符号,例如~Person()
。析构函数通常不接受参数。
构造函数和析构函数示例:
class Example {
private:
int* data;
public:
// 构造函数
Example(int value) {
data = new int(value); // 动态分配内存
std::cout << "Constructor called!" << std::endl;
}
// 析构函数
~Example() {
delete data; // 释放动态分配的内存
std::cout << "Destructor called!" << std::endl;
}
};
这里的构造函数写法是使用成员初始化列表的方式来初始化成员变量:
Person(std::string n, int a) : name(n), age(a) {}
而你提到的另一种方式是通过在构造函数体内赋值:
Person(std::string n, int a) { name = n; age = a; }
虽然这两种方式在效果上类似,都能初始化对象的成员变量,但使用成员初始化列表通常是更推荐的方式,原因如下:
- 更高效
对于内置类型(如int
、double
),两种方式的区别可能不大。然而,对于非内置类型的成员变量,使用成员初始化列表可以避免不必要的默认构造和赋值操作。
-
在使用成员初始化列表的情况下,成员变量在对象构造时直接被初始化:
Person(std::string n, int a) : name(n), age(a) {}
这里,
name
和age
直接在初始化时被赋值为n
和a
,没有多余的步骤。 -
如果你在构造函数体内赋值:
Person(std::string n, int a) { name = n; age = a; }
在这种情况下,
name
和age
首先会通过它们的默认构造函数初始化(对于内置类型可能无影响,但对于类对象会多一步默认构造),然后再通过赋值操作来更新它们的值。这会导致不必要的性能开销,尤其是在类的成员变量是复杂类型(如std::string
、自定义类)时。
- 不可重新赋值的成员(
const
或引用
)
成员初始化列表是初始化const
成员或引用成员的唯一方式。因为const
或引用类型的成员变量在对象创建时就必须被初始化,不能通过赋值进行修改。
例如:
class Person {
private:
const int id;
std::string name;
public:
// 必须使用成员初始化列表初始化 const 成员
Person(int i, std::string n) : id(i), name(n) {}
};
这里 id
是 const
,只能在成员初始化列表中被初始化,不能在构造函数体内进行赋值。
访问控制修饰符
private
:私有成员只能在类的内部访问,不能在类的外部访问。通常用于隐藏对象的内部实现。public
:公有成员可以在类的外部访问,是类的对外接口。protected
:受保护的成员只能在类的内部以及派生类中访问,不能在类的外部访问。
继承(Inheritance)
继承允许一个类从另一个类继承属性和方法。通过继承,子类可以扩展或修改父类的行为。继承的方式有三种:
- 公有继承(public inheritance):父类的
public
和protected
成员在子类中保持原有的访问级别。 - 私有继承(private inheritance):父类的所有成员在子类中都变为
private
访问级别。 - 保护继承(protected inheritance):父类的
public
和protected
成员在子类中变为protected
。
继承示例:
class Animal {
public:
void eat() {
std::cout << "I am eating." << std::endl;
}
};
class Dog : public Animal {
public:
void bark() {
std::cout << "I am barking." << std::endl;
}
};
虚函数
虚函数(Virtual Function) 是 C++ 中用于实现运行时多态性的一种机制。它允许在继承体系中,通过基类的指针或引用,调用子类的重写函数。
虚函数的主要目的是在程序运行时,根据对象的实际类型决定调用哪个函数版本,而不是在编译时决定。
- 定义:在基类中用 virtual 关键字声明的函数就是虚函数。
- 多态性:虚函数支持动态绑定(也称为后期绑定),这意味着函数调用会在运行时解析,而不是在编译时确定。
- 重写:子类可以通过重写基类的虚函数来提供不同的实现。
- 基类指针/引用:通过基类的指针或引用调用虚函数时,程序会根据实际的对象类型选择正确的重写版本。
class Animal {
public:
virtual void sound() const {
std::cout << "Animal makes a sound." << std::endl;
}
};
class Dog : public Animal {
public:
void sound() const override {
std::cout << "Dog barks." << std::endl;
}
};
inline 和 类
当你在类的声明中定义了一个成员函数,除了虚函数之外,编译器会自动将这些函数视为内联函数(inline
),即使你没有显式地使用 inline
关键字。这意味着编译器会尝试将这些函数作为内联函数处理。
示例:
class MyClass {
public:
int getValue() { return value; } // 隐式内联函数
void setValue(int v) { value = v; } // 隐式内联函数
virtual void display() { // 虚函数,不会隐式内联
std::cout << "Value: " << value << std::endl;
}
private:
int value;
};
在这个例子中,getValue()
和 setValue()
是在类的声明中定义的。根据这句话的意思,它们被隐式地视为内联函数。因此,编译器可能会将这些函数的代码直接插入到调用这些函数的地方,类似于手动使用 inline
关键字的效果。
解释:
-
类声明中的函数定义:如果你在类的声明中直接定义了函数(即在类的头文件中直接写了函数体),那么这些函数会被自动认为是内联的。
class MyClass { public: int getValue() { return value; } // 隐式内联 private: int value; };
-
虚函数的例外情况:虚函数不会被自动内联。虚函数是通过虚函数表(vtable)进行动态调用的,因此它们不适合内联优化。内联函数的优势是消除函数调用开销,而虚函数的调用机制本身就是为了支持动态多态性,这会导致函数调用必须在运行时决定。
class MyClass { public: virtual void display() { std::cout << "Hello" << std::endl; } // 不会隐式内联 };
内联的自动化与限制:
-
自动内联:类声明中的非虚函数会被编译器自动标记为内联函数,不需要显式写出
inline
关键字。 -
编译器的自由裁量权:虽然函数在类声明中定义时会自动成为内联函数,但编译器仍然有权决定是否实际将这些函数内联到调用点。如果函数过于复杂或体积过大,编译器可能不会内联它。
优点与使用场景:
-
提高性能:自动内联函数可以减少函数调用开销,因为函数的代码可能会被直接插入到调用点,省去了压栈、跳转和返回的开销。
-
简单函数:这种内联机制非常适用于简单且频繁调用的函数,例如访问器(
getters
和setters
)以及其他短小的操作函数。