本文最后更新于40 天前,其中的信息可能已经过时,如有错误请发送邮件到zhangweihao22@outlook.com
在C++中使用一些函数为什么还需要在它的前面添加std::?
- 名称空间(namespace)的作用
- 在 C++ 中,
std
是标准库(Standard Library)的名称空间。名称空间主要用于避免命名冲突。C++ 标准库包含了大量的函数、类和对象,如pow
、sqrt
等数学函数。如果没有名称空间,当用户自己定义的函数或者引入的其他库中有和标准库相同名字的函数时,编译器就无法区分应该使用哪一个,这会导致编译错误或者程序逻辑混乱。 - 例如,假设用户自己写了一个名为
pow
的函数用于其他目的,没有名称空间隔离的话,编译器就不知道在代码中出现pow
时是要调用用户自定义的函数还是标准库中的数学pow
函数。
- 在 C++ 中,
std::
的具体用途std::pow
这种写法明确告诉编译器,要使用的pow
函数是位于std
名称空间下的,也就是 C++ 标准库中的pow
函数。同样,std::sqrt
也是如此,它表明调用的是标准库中的平方根函数。- 如果不使用
std::
前缀(在某些特定情况下可以,比如通过using
指令或声明),编译器会找不到函数定义,因为它不知道去哪里寻找pow
和sqrt
函数。例如,在一个未使用using
指令或声明,并且没有std::
前缀的情况下写pow(2, 3)
,编译器会报错,提示找不到pow
函数的定义。
- 其他使用方式(
using
指令或声明)- 除了每次使用函数时都加上
std::
前缀这种方式,还可以通过using
指令或声明来简化代码。 using
指令:例如using namespace std;
,这条指令会将整个std
名称空间引入当前的代码范围。这样在后续代码中就可以直接使用pow
和sqrt
等函数而不需要写std::
前缀了。不过这种方式有一定的风险,因为它可能会引入很多不必要的名称,增加命名冲突的可能性。using
声明:例如using std::pow;
和using std::sqrt;
,这种方式只引入特定的函数(如pow
和sqrt
)到当前代码范围,这样在后续代码中就可以直接使用这些函数而不需要std::
前缀,相对using namespace std;
来说更安全一些,因为它只引入了需要的函数,减少了命名冲突的范围。
- 除了每次使用函数时都加上
标准库中的数学函数是在哪个头文件中声明的?
不使用std::前缀调用标准库函数的方法有哪些?
除了std::,C++中还有哪些常用的名称空间?
C++中的.操作符
解释
- 成员访问操作符(针对对象)
- 在 C++ 中,“.” 是成员访问操作符,用于访问类或结构体对象的成员。当你有一个类或者结构体的实例(对象)时,通过 “.” 操作符可以访问该对象的成员变量和成员函数。
- 例如,假设有一个简单的类
Circle
,用于表示圆,它有一个成员变量radius
(半径)和一个成员函数area
(计算面积):
class Circle { public: double radius; double area() { return 3.14159 * radius * radius; } };
- 可以创建一个
Circle
类的对象,并使用 “.” 操作符来访问它的成员:
Circle myCircle; myCircle.radius = 2.0; //使用.操作符访问radius成员变量并赋值 double circleArea = myCircle.area(); //使用.操作符访问area成员函数并调用
- 这里的
myCircle.radius
表示访问myCircle
这个对象的radius
成员变量,myCircle.area()
表示调用myCircle
这个对象的area
成员函数。
- 与指针访问成员操作符(
->
)的对比- 当处理指向对象的指针时,使用
->
操作符来访问对象的成员。而 “.” 操作符用于直接访问对象(非指针)的成员。 - 例如,假设有一个
Circle
类的指针pCircle
:cpp Circle* pCircle = new Circle(); pCircle->radius = 3.0; //使用->操作符访问radius成员变量并赋值,因为pCircle是指针 double circleArea = pCircle->area(); //使用->操作符访问area成员函数并调用
- 如果想要使用 “.” 操作符来访问指针所指向对象的成员,需要先对指针进行解引用(
*
操作符),例如:cpp (*pCircle).radius = 3.0; double circleArea = (*pCircle).area();
- 不过这种写法比较繁琐,通常在处理指针时更倾向于使用
->
操作符。
- 当处理指向对象的指针时,使用
为什么使用一些函数的时候会在函数前使用.运算符?
因为该函数时对应的一个成员函数【被封装在了一个类或者结构体中】
【发生这个疑惑时建议去看C++中的类和结构体部分】
例如:
vector<int> vec8;
vec8.insert(vec8.begin(), vec2.begin(), vec2.begin() + 3);
为什么在使用Insert之前使用.运算符?
insert
是std::vector
类的成员函数- 在 C++ 中,
std::vector
是一个类模板,不是结构体。insert
是std::vector
类的成员函数。这意味着insert
函数是与std::vector
这个类紧密相关的,它的实现和功能是专门为std::vector
容器的操作而设计的。 - 例如,
std::vector
内部维护了一个动态分配的数组来存储元素,insert
函数的实现需要考虑如何在这个数组的指定位置插入新元素。这可能涉及到移动现有元素以腾出空间、更新内部的大小和容量等信息。
- 在 C++ 中,
- 成员函数与类的关系
- 作为成员函数,
insert
可以访问std::vector
类的私有成员和保护成员。这些成员包括存储元素的底层数组指针、容器的大小(当前元素个数)、容量(可容纳元素的最大数量)等信息。 - 这种封装性使得
insert
函数能够以一种安全、高效且符合std::vector
内部数据结构特点的方式进行插入操作。例如,insert
函数会根据std::vector
的当前容量来判断是否需要重新分配内存,以确保能够成功插入新元素并且保持std::vector
的其他操作(如随机访问、遍历等)的正确性和高效性。
- 作为成员函数,
vector数组
基本解释
- 定义
- 在 C++ 中,
std::vector
是一个标准模板库(STL)中的容器,它可以看作是一个动态大小的数组。与普通数组不同的是,它能够自动管理内存,在需要的时候动态地扩展或收缩存储元素的空间。 - 例如,普通数组在定义时就需要指定大小,像
int arr[5];
,这个数组大小是固定的为 5,不能随意改变大小。而std::vector<int> vec;
定义了一个int
类型的vector
,初始时可以为空,之后可以方便地添加或删除元素。
- 在 C++ 中,
- 作用
- 存储数据:
- 可以用来存储同一种类型的多个数据。例如,存储一组整数用于统计学生成绩
std::vector<int> scores;
,或者存储一组字符串用于记录文件名std::vector<std::string> fileNames;
。
- 可以用来存储同一种类型的多个数据。例如,存储一组整数用于统计学生成绩
- 动态调整大小:
- 这是
std::vector
的一个重要特性。当需要添加元素时,如果当前分配的空间足够,它会直接将元素添加到容器中;如果空间不足,它会自动重新分配一块更大的内存空间,将原来的元素复制或移动到新空间,并添加新元素。例如:cpp std::vector<int> numbers; numbers.push_back(1); //添加元素1,此时vector自动管理内存,分配空间来存储这个元素 numbers.push_back(2); //添加元素2,vector可能会根据内部策略扩展内存空间
- 这是
- 高效的随机访问:
- 提供了类似于数组的随机访问方式。可以使用下标运算符
[]
来访问vector
中的元素,就像访问普通数组一样。例如:cpp std::vector<int> vec = {1, 2, 3, 4, 5}; int secondElement = vec[1]; //访问第二个元素,结果为2
- 这种随机访问方式的时间复杂度是常数时间 ,这使得
vector
在需要频繁访问元素的场景下非常高效,比如在数值计算、数据查询等应用中。
- 提供了类似于数组的随机访问方式。可以使用下标运算符
- 支持多种操作:
- 除了添加(
push_back
)和访问元素外,vector
还支持很多其他操作。例如,可以删除最后一个元素(pop_back
),在指定位置插入元素(insert
),删除指定位置的元素(erase
),获取元素个数(size
),获取当前容量(capacity
)等操作。例如:cpp std::vector<int> vec = {1, 2, 3, 4, 5}; vec.pop_back(); //删除最后一个元素,现在vec为{1, 2, 3, 4} vec.insert(vec.begin() + 1, 6); //在第二个位置插入6,现在vec为{1, 6, 2, 3, 4}
- 除了添加(
- 存储数据:
vector+结构体【高级操作】
vectorstudents; 这样和直接用结构体定义有什么区别?
- 存储方式和灵活性
- **`vector<Student>`**:
- `vector<Student>`是一个动态大小的容器,用于存储`Student`类型的对象。它可以方便地改变大小,能够在运行时根据需要添加或删除`Student`对象。例如,可以使用`students.push_back(s1)`来添加一个新的`Student`对象,其中`s1`是一个`Student`类型的实例。这种动态性使得它在处理不确定数量的`Student`对象时非常方便,比如从文件中读取学生信息,事先不知道学生数量,就可以使用`vector<Student>`来动态存储。
- 它还支持许多高级的操作,如排序(如果为`Student`类型定义了合适的比较函数)、插入(`insert`函数)、删除(`erase`函数)等,这些操作可以方便地对学生对象集合进行管理。例如,可以按照学生的成绩对`vector<Student>`中的学生对象进行排序。
- **结构体定义(假设是普通的结构体数组)**:
- 如果直接用结构体定义数组,如`Student studentsArray[10];`(假设定义了一个大小为 10 的数组),其大小在编译时就已经确定,是固定的。这意味着如果想要存储超过 10 个学生对象,就需要重新定义一个更大的数组或者采用其他复杂的策略。
- 对于这种固定大小的结构体数组,操作相对比较有限。例如,插入一个新的学生对象到数组中间位置可能需要手动移动后面的元素来腾出空间,删除一个元素也需要手动处理数组元素的移动,不像`vector`有内置的高效插入和删除函数。
- 内存管理方面
vector<Student>
:vector
会自动管理内存。它在内部会根据元素数量的变化动态地分配和释放内存。当添加元素导致当前内存空间不足时,vector
会自动分配一块更大的内存空间,将原有的元素复制或移动到新空间,并释放旧的内存空间。这种自动内存管理机制减少了程序员因内存管理不当而导致错误的风险,如内存泄漏或悬空指针等问题。
- 结构体定义(普通数组):
- 对于结构体数组,程序员需要自己负责确保数组的大小足够,并且如果涉及到动态内存分配(例如,数组元素是动态分配的结构体),需要手动释放内存。例如,如果
Student
结构体中的成员包含了动态分配的内存(如char*
类型的成员用于存储学生姓名的字符串),在释放数组时,需要正确地释放每个结构体元素中的动态分配内存,否则可能会导致内存泄漏。
- 对于结构体数组,程序员需要自己负责确保数组的大小足够,并且如果涉及到动态内存分配(例如,数组元素是动态分配的结构体),需要手动释放内存。例如,如果
- 遍历和访问方式
vector<Student>
:- 可以使用迭代器或者范围 – based for 循环来方便地遍历
vector
中的Student
对象。例如,使用迭代器遍历:
for (vector<Student>::iterator it = students.begin(); it!= students.end(); ++it) { cout << it->name << " " << it->age << endl; }
- 也可以使用范围 – based for 循环:
for (Student& student : students) { cout << student.name << " " << student.age << endl; }
- 可以使用迭代器或者范围 – based for 循环来方便地遍历
- 结构体定义(普通数组):
- 对于结构体数组,通常使用索引来遍历,如:
for (int i = 0; i < 10;++i) { cout << studentsArray[i].name << " " << studentsArray[i].age << endl; }
- 这种方式在数组大小固定且已知的情况下比较直观,但对于动态大小的情况就不太方便了。
vector动态数组的内置函数
#### size()函数
#### data()函数
#### clear()函数
#### resize();函数
迭代器
- 定义
- 迭代器(Iterator)是一种设计模式,用于遍历容器(如数组、链表、树、图等数据结构)中的元素。在编程语言中,迭代器提供了一种统一的方式来访问容器内的元素,而无需了解容器的内部结构。
- 作用原理
- 以一个简单的数组容器为例,假设我们有一个整数数组
int arr[] = {1, 2, 3, 4, 5}
。如果没有迭代器,要访问数组中的元素,我们可能需要使用索引来遍历,像这样:cpp for(int i = 0; i < sizeof(arr)/sizeof(arr[0]); ++i) { std::cout << arr[i] << " "; }
- 当有了迭代器后,迭代器就像是一个指向容器元素的 “指针”,可以通过移动这个 “指针” 来逐个访问元素。对于上述数组,在 C++ 中可以这样使用迭代器(假设使用
std::begin
和std::end
函数获取迭代器范围):cpp #include <iostream> #include <iterator> int main() { int arr[] = {1, 2, 3, 4, 5}; int* begin_iter = std::begin(arr); int* end_iter = std::end(arr); for(int* it = begin_iter; it!= end_iter; ++it) { std::cout << *it << " "; } return 0; }
- 这里的
it
就是迭代器,它从begin_iter
(指向数组第一个元素)开始,每次移动到下一个元素(通过++it
),直到it
到达end_iter
(指向数组最后一个元素之后的位置)。
- 以一个简单的数组容器为例,假设我们有一个整数数组
- 在不同编程语言中的应用
- C++:
- C++ 标准模板库(STL)广泛使用迭代器。例如,
std::vector
容器有自己的迭代器类型。可以通过vector
的begin
和end
方法获取迭代器。begin
返回指向第一个元素的迭代器,end
返回指向最后一个元素之后的迭代器。除了vector
,其他容器如list
、set
、map
等也都有各自的迭代器,它们的操作方式根据容器的内部结构有所不同。 - C++ 中的迭代器还分为不同的类别,如输入迭代器(Input Iterator)、输出迭代器(Output Iterator)、前向迭代器(Forward Iterator)、双向迭代器(Bidirectional Iterator)和随机访问迭代器(Random – Access Iterator)。这些不同类型的迭代器具有不同的功能和性能特点。例如,随机访问迭代器可以像操作数组索引一样,通过
it + n
的方式直接访问容器中距离当前元素n
个位置的元素,而输入迭代器可能只支持单向的、只读的遍历。
- C++ 标准模板库(STL)广泛使用迭代器。例如,
- Java:
- 在 Java 中,迭代器主要用于遍历集合类(Collection)。例如,对于
ArrayList
和LinkedList
等集合,都可以使用Iterator
接口来遍历。java.util.Iterator
接口提供了hasNext
方法用于检查是否还有下一个元素,以及next
方法用于获取下一个元素。以下是一个简单的示例:java import java.util.ArrayList; import java.util.Iterator; public class IteratorExample { public static void main(String[] args) { ArrayList<Integer> list = new ArrayList<>(); list.add(1); list.add(2); list.add(3); Iterator<Integer> iterator = list.iterator(); while(iterator.hasNext()) { System::println(iterator.next()); } } }
- 在 Java 中,迭代器主要用于遍历集合类(Collection)。例如,对于
- Python:
- Python 中的迭代器是一种实现了
__iter__
和__next__
方法的对象。例如,对于一个列表(List),可以通过iter
函数获取迭代器,然后使用next
函数逐个获取元素。同时,Python 的for
循环实际上是在内部使用了迭代器机制。例如:Python my_list = [1, 2, 3, 4] my_iter = iter(my_list) try: while True: print(next(my_iter)) except StopIteration: pass
- 并且,Python 中的生成器(Generator)也是一种特殊的迭代器,它可以通过
yield
语句来暂停函数的执行,返回一个值,并且在下次调用时从暂停的位置继续执行。例如:python def my_generator(): for i in range(1, 5): yield i gen = my_generator() for value in gen: print(value)
- Python 中的迭代器是一种实现了
- C++:
类和对象
核心代码:
#include<iostream>
using namespace std;
// class代表设计一个类,类后面紧跟着的就是类的名称
class Circle {
// 访问权限
// 公共权限
public:
// 类中的属性和行为统称为成员
// 属性
int m_r;
// 行为
double calculateZC() {
return 2 * PI * m_r;
}
};
int main () {
// 通过圆类,创建具体的圆【对象】
// 实例化
Circle c1;
// 给圆对象的属性进行赋值
c1.m_r = 10;
cout << "圆的周长为:"<<c1.calculateZC()<<endl;
return 0;
}
属性和行为
访问权限及其设置
公共权限 成员 类内可以访问,类外可以访问
保护权限 成员 类内可以访问,类外不可以访问 子类可以访问父类
私有权限 成员 类内可以访问,类外不可以访问 子类不可以访问父类
// 后两者在继承部分有区别
protected 和 private的作用
- 可以自己控制读写权限
- 对于写可以检测数据有效性
- 私有(private)权限
- 信息隐藏:
- 私有成员(包括变量和函数)主要用于信息隐藏。这是面向对象编程中的一个重要概念,通过将类的内部实现细节隐藏起来,只暴露必要的接口(通常是公有成员函数)给外部世界,使得类的使用者不需要了解类的内部工作机制,只需要关心如何正确地使用这些接口。例如,考虑一个表示银行账户的类
BankAccount
:cpp class BankAccount { private: double balance; // 账户余额,私有成员 public: void deposit(double amount) { if (amount > 0) { balance += amount; } } void withdraw(double amount) { if (amount > 0 && balance >= amount) { balance -= amount; } } double getBalance() const { return balance; } };
- 在这里,
balance
是私有成员,用户不能直接访问和修改它。这样可以防止外部代码随意篡改账户余额,保证了数据的安全性和一致性。用户只能通过deposit
、withdraw
和getBalance
这些公有函数来操作账户余额。
- 私有成员(包括变量和函数)主要用于信息隐藏。这是面向对象编程中的一个重要概念,通过将类的内部实现细节隐藏起来,只暴露必要的接口(通常是公有成员函数)给外部世界,使得类的使用者不需要了解类的内部工作机制,只需要关心如何正确地使用这些接口。例如,考虑一个表示银行账户的类
- 限制访问范围:
- 私有成员只能在类的内部被访问。这意味着只有类的成员函数和友元函数(如果有)可以访问私有成员。这种限制有助于防止意外的修改和错误的使用。例如,如果
BankAccount
类还有其他成员函数,它们可以访问balance
来执行一些复杂的业务逻辑,如计算利息、检查账户状态等,但外部代码不能绕过这些规则直接操作balance
。
- 私有成员只能在类的内部被访问。这意味着只有类的成员函数和友元函数(如果有)可以访问私有成员。这种限制有助于防止意外的修改和错误的使用。例如,如果
- 信息隐藏:
- 保护(protected)权限
- 继承中的访问控制:
- 保护成员主要用于在继承关系中提供一种特殊的访问级别。在派生类(子类)中,保护成员的访问权限介于公有和私有之间。派生类可以访问基类(父类)的保护成员,就好像它们是自己的成员一样,但对于外部世界(非派生类),保护成员的行为类似于私有成员,不能直接访问。例如,考虑一个简单的图形基类
Shape
和一个派生类Rectangle
:cpp class Shape { protected: double areaValue; // 图形面积,保护成员 public: Shape() : areaValue(0.0) {} virtual double getArea() const = 0; }; class Rectangle : public Shape { private: double length; double width; public: Rectangle(double l, double w) : length(l), width(w) { areaValue = length * width; // 派生类可以访问基类的保护成员areaValue } double getArea() const override { return areaValue; } };
- 在这个例子中,
Shape
类中的areaValue
是保护成员。Rectangle
作为Shape
的派生类,可以访问和修改areaValue
来计算和存储矩形的面积。这种设计使得在继承体系中,派生类能够在一定程度上继承和扩展基类的属性,同时又能限制外部对这些属性的访问。
- 保护成员主要用于在继承关系中提供一种特殊的访问级别。在派生类(子类)中,保护成员的访问权限介于公有和私有之间。派生类可以访问基类(父类)的保护成员,就好像它们是自己的成员一样,但对于外部世界(非派生类),保护成员的行为类似于私有成员,不能直接访问。例如,考虑一个简单的图形基类
- 代码复用和扩展性:
- 保护成员有助于代码的复用和扩展。在设计类层次结构时,保护成员可以被多个派生类共享和修改,以适应不同的具体需求。例如,如果要创建一个新的派生类
Square
(正方形),它也可以访问Shape
类中的areaValue
保护成员来计算和存储自己的面积,从而复用了基类的部分代码结构,同时可以根据自身的特点(如正方形边长相等)进行特殊的实现。
- 保护成员有助于代码的复用和扩展。在设计类层次结构时,保护成员可以被多个派生类共享和修改,以适应不同的具体需求。例如,如果要创建一个新的派生类
- 继承中的访问控制:
struct 和 class区别
主要区别是 默认的权限不同
- struct默认权限为公共
- class默认权限为私有
构造函数和析构函数
演示代码:
#include <iostream>
using namespace std;
// 对象的初始化和清理
// 构造函数 进行初始化操作
// 析构函数 进行清理的操作
// 【应该都是本身就具有相应的功能,但是能够自己函数,在调用该函数的时候同步实现自己的代码】
class Person {
// 构造函数
// 没有返回值 不用写void
// 函数名与类名相同
// 构造函数可以有参数,可以发生重载
// 创建对象的时候,构造函数会自动调用一次,而且只调用一次
public:
Person() {
cout<<"Person 构造函数的调用"<<endl;
}
// 析构函数 进行清理操作
// 没有返回值 不写void
// 函数名和类名相同 在名称前加~
// 析构函数不可以有参数 不可以发生重载
// 对象在销毁前会自动调用析构函数,且只调用一次
~Person() {
cout<<"Person 析构函数的调用"<<endl;
}
};
void test () {
Person p; // 栈上的数据,函数执行完后就会释放这个对象
}
int main () {
test();
return 0;
}
构造函数的分类和调用
两种分类方式:
- 按参数分为:有参构造和无参构造
- 按类型分为:普通构造和拷贝构造
三种调用方式: - 括号法
- 显示法
- 隐式转换法
示例代码:
#include<iostream>
using namespace std;
class Person {
// 构造函数
Person() {
cout<<"Person 无参构造函数的调用"<<endl;
}
Person(int a) {
age = a;
cout<<"Person 有参构造函数的调用"<<endl;
}
// 拷贝构造函数
Person (const Person &p) {
age = p.age
cout<<"Person 拷贝构造函数的调用"<<endl;
}
~Person() {
cout<<"Person 析构函数的调用"<<endl;
}
int age;
};
void test() {
// 括号法
Person p1;
Person p2(10);
Person p3(p2);
// 注意:
// 调用默认构造函数的时候不要加()
// 因为下面的这行代码,编译器会认为是一个函数的声明,不会认为在创建对象
Person p1();
// 就像下面的这个函数声明一样
void function;
// 显示法
Person p1;
Person p2 = Person (10);
Person p3 = Person (p2);
Person (10); // 匿名对象 特点:当前执行结束后,系统会立即回收掉匿名对象
// 注意:
// 不要利用拷贝构造函数初始化匿名对象 编译器会认为 Person (p3) === Person p3;看作对象声明
// 隐式转换法
Person p4 = 10; // 相当于写了Person p4 = Person (10);
}
int main () {
test();
return 0;
}