C++基础知识
- 常见
- 少见
- QT MFC
- 面试题
常见
指针与引用的区别
定义和性质:
指针是一个实体,它存储的是另一个变量的地址,通过这个地址可以访问到该变量的值;引用则相当于是原变量的另一个名字,它和原变量在内存中占据同一地址。
初始化和使用:
引用必须在声明时初始化,并且一旦与某个变量绑定后就不能再改变指向其他的变量;而指针可以先声明再初始化,并且可以在任何时候改变指向不同的变量。
内存占用:
「sizeof 引用」得到的是所指向的变量的大小,而「sizeof 指针」得到的是指针本身的大小。
类型安全性:
引用是类型安全的,编译器会在编译时进行类型检查;而指针不是类型安全的,可以通过强制类型转换等方式指向不同类型的变量。
面向对象的三个基本特征
封装:封装是指将数据(属性)和操作这些数据的方法组合在一起,形成一个类。通过封装,可以隐藏对象的具体实现细节,只暴露出必要的接口供外部使用。这样做的目的是为了提高代码的复用性、安全性和维护性。
继承:继承允许创建新的类,这个新类被称为子类或派生类,它自动拥有父类的所有属性和方法。此外,子类还可以添加新的属性和方法或者重写父类的方法。继承支持了代码的扩展性和模块化,使得程序结构更加清晰,易于管理和维护。
多态:多态是指允许不同类的对象对同一消息作出响应的能力。在C++中,多态主要通过虚函数来实现。当一个基类类型的指针指向一个派生类的对象时,可以通过这个指针调用派生类中的虚函数,这样就实现了运行时的多态性。多态性提高了程序的灵活性和扩展性。
菱形继承
菱形继承是指在面向对象编程中的一种继承结构,其中两个子类分别继承自同一个基类,然后还有一个派生类继承自这两个子类,从而形成一个菱形形状的继承层次。这种情况下,如果没有适当的处理,可能会导致数据成员的二重继承问题,也就是说,最终的派生类中会包含两份来自共同基类的实例,这显然不是设计者的初衷。
在C++中,菱形继承的问题可以通过虚拟继承解决。虚拟继承是为了避免因多继承产生的数据冗余而引入的概念。当声明一个基类为虚拟基类时,它的数据只会被继承一次,所有直接或间接继承这个虚拟基类的派生类共享同一份基类数据成员的副本。
下面是一个简单的菱形继承结构的例子,展示没有使用虚拟继承和使用虚拟继承两种情况:
// 没有使用虚拟继承的情况
class Base {
public:
Base(int value) : data(value) {}
int data;
};
class Derived1 : public Base {
public:
Derived1(int value) : Base(value) {}
};
class Derived2 : public Base {
public:
Derived2(int value) : Base(value) {}
};
class DiamondDerived : public Derived1, public Derived2 {
public:
DiamondDerived(int value) : Derived1(value), Derived2(value) {}
// 在此例中,DiamondDerived有两个Base::data的副本
};
// 使用虚拟继承的情况
class VirtualBase {
public:
VirtualBase(int value) : data(value) {}
virtual ~VirtualBase() {} // 虚拟基类需要有一个虚析构函数
int data;
};
class VirtualDerived1 : virtual public VirtualBase {
public:
VirtualDerived1(int value) : VirtualBase(value) {}
};
class VirtualDerived2 : virtual public VirtualBase {
public:
VirtualDerived2(int value) : VirtualBase(value) {}
};
class VirtualDiamondDerived : public VirtualDerived1, public VirtualDerived2 {
public:
VirtualDiamondDerived(int value) : VirtualDerived1(value), VirtualDerived2(value) {}
// 在此例中,VirtualDiamondDerived只有一个VirtualBase::data的副本
};
在上述例子中,对于没有使用虚拟继承的菱形继承,DiamondDerived
类会有两个 Base::data
成员;而采用虚拟继承的 VirtualDiamondDerived
类则只包含一个 VirtualBase::data
实例,避免了数据冗余。同时需要注意的是,虚拟基类通常需要提供一个虚析构函数,这是因为在多态环境下,为了正确释放资源,编译器需要能够找到虚拟基类的析构函数。
多态
在面向对象的程序设计中,一个接口,多种实现即为多态。c++的多态性具体体现在编译和运行两个阶段。
编译时多态是静态多态,在编译时就可以确定使用的接口, 地址是早绑定。运行时多态是动态多态,具体引用的接口在运行时才能确定,地址是晚绑定。
C++多态的原理主要基于虚函数(virtual function)的概念。在C++中,多态性可以通过两种方式实现:编译时多态和运行时多态。
静态多态
主要通过函数重载或运算符重载来实现。比如说,相同的函数名,可以通过不同的形参重载出不同的函数,这就是多态的特性,但这里多态是编译阶段完成,也就是说编译器会将函数绑定到唯一确定的形式上去,这就是静态多态。
动态多态
动态多态是通过虚函数和类的继承来实现的。动态多态需要满足如下条件:
1.有继承关系;
2.子类要重写基类的虚函数(带virtual的函数);
3.必须是通过基类的指针或者引用调用虚函数
#include <iostream>
class Animal {
public:
virtual void makeSound() const { std::cout << "动物发出声音" << std::endl; } // 定义虚函数
};
class Dog : public Animal {
public:
void makeSound() const override { std::cout << "狗汪汪叫" << std::endl; } // 重写虚函数
};
class Cat : public Animal {
public:
void makeSound() const override { std::cout << "猫喵喵叫" << std::endl; } // 重写虚函数
};
void callMakeSound(Animal& animal) {
animal.makeSound(); // 动态多态,运行时根据animal指向的实际对象类型调用相应的makeSound()函数
}
int main() {
Dog dog;
Cat cat;
Animal& animalDog = dog;
Animal& animalCat = cat;
//Animal *animalDog = new Dog; //需要释放内存
//Animal* animalCat = new Cat; //需要释放内存
callMakeSound(animalDog); // 输出“狗汪汪叫”
callMakeSound(animalCat); // 输出“猫喵喵叫”
return 0;
}
重载 重写 重定义
重写/覆盖(override):(不在同一个作用域)函数返回值类型,函数名,参数列表完全一致称为重写
重载(overload):(同一作用域下)函数名相同,参数类型、个数、顺序不同,返回值可相同可不同 重写是动态态绑定的多态,而重载是静态绑定的多态 。
重定义/隐藏:(不在同一个作用域)函数名字相同,返回值可相同可不同,参数有两种情况①参数不同。此时,不论有无virtual 关键字,基类的函数将被隐藏(注意别与重载以及覆盖混淆)②参数相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)
在C++中,“重定义”(Redefinition)一词可以有两种含义:
-
局部作用域内的重定义:
在同一个作用域内,不能定义两个同名的函数或变量,否则就是非法的重定义,会导致编译错误。这是因为编译器无法确定应该使用哪一个定义。例如:void foo() {...} // 第一次定义 void foo() {...} // 错误:重定义,同一个作用域内重复定义同名函数 int x = 10; // 第一次定义 int x = 20; // 错误:重定义,同一个作用域内重复定义同名变量
-
继承关系中的重定义:
在类继承关系中,子类可以重新定义(或称隐藏)父类的非虚函数,即使参数列表不同,这也被称为重定义(或有时候称为隐藏)。这种情况下,子类中的同名函数会屏蔽父类中的同名函数,但并不会实现多态性。如果想要实现多态,父类的函数需要标记为virtual
,并且子类以同样的签名重新定义该函数,则称之为函数的重写(Override)。class Base { public: void func(int a) {...} // 非虚函数 }; class Derived : public Base { public: void func(double b) {...} // 非虚函数,重定义(隐藏)了父类的func(int) }; // 如果是虚函数,则如下所示 class Base { public: virtual void virtualFunc(int a) {...} // 虚函数 }; class Derived : public Base { public: void virtualFunc(int a) override {...} // 重写(Override)了父类的virtualFunc(int) };
当子类中的函数与父类中的非虚函数重名时,即使参数不同,也会隐藏父类的所有同名函数(不论是否为虚函数)。如果希望在派生类中维持多态行为,应该确保父类中的函数是虚函数,并且子类中的重定义函数应当具有与父类相同的参数列表和返回类型(或兼容的返回类型)。
析构函数为什么用虚函数
若使用基类指针操作派生类,需要防止在析构时,只析构基类,而不析构派生类。
但是,如果析构函数不被声明成虚函数,则编译器采用的绑定方式是静态绑定,在删除基类指针时,
只会调用基类析构函数,而不调用派生类析构函数,这样就会导致基类指针指向的派生类对象析构不完全。
若是将析构函数声明为虚函数,则可以解决此问题。
纯虚函数
纯虚函数是C++中一种特殊的虚函数,它用于在基类中定义一个接口,但不提供具体的实现。具体来说,纯虚函数的原理包括以下几个方面:
- 声明形式:纯虚函数在声明时,会在函数签名后面加上
= 0
,这是纯虚函数的标志。- 抽象类:包含纯虚函数的类被称为抽象类。由于抽象类中的纯虚函数没有实现,因此抽象类不能直接实例化。
- 派生类实现:派生类继承抽象类后,必须重写所有的纯虚函数,并提供具体的实现。这样,派生类的对象才能被创建,并且可以通过多态性调用这些函数。
- 规范行为:纯虚函数通常用来规范派生类的行为,即定义接口。这样做的目的是为了让派生类根据基类的规范来实现具体的功能。
- 指向实现的指针或引用:虽然抽象类不能实例化,但可以声明指向实现该抽象类的具体类的指针或引用。这样,可以通过这些指针或引用来调用派生类中实现的纯虚函数。
- 多态性:纯虚函数是实现多态性的一种方式。通过在不同的派生类中对同一个纯虚函数提供不同的实现,可以在运行时根据对象的实际类型来调用相应的函数实现。
- 设计模式:纯虚函数常用于设计模式中,如工厂模式、策略模式等,它们依赖于抽象类来定义算法骨架,而将具体实现留给派生类。
总的来说,纯虚函数的作用在于提供一个接口框架,要求所有派生自抽象类的子类都必须实现这个接口,从而确保了所有子类都有相同的函数签名和预期的行为。这种机制提高了代码的可扩展性和可维护性。
new/delete 与 malloc/free 区别
类型与语言:
new/delete是C++中的操作符,而malloc/free是C语言中的库函数。使用new/delete需要编译器的支持,而malloc/free需要包含相应的头文件才能使用。
重载:
new 、delete 是操作符,可以重载,只能在C++ 中使用。 malloc、free 是函数,可以覆盖,C、C++ 中都可以使用。
是否调用构造与析构函数:
new 可以调用对象的构造函数,对应的delete 调用相应的析构函数。malloc 仅仅分配内存,free 仅仅回收内存,并不执行构造和析构函数
内存分配方式:
new在分配内存时会根据数据类型自动计算所需内存的大小,而malloc需要开发者显式指定所需的内存大小。
变量的声明和定义有什么区别
变量的定义为变量分配地址和存储空间,变量的声明不分配地址。一个变量可以在多个地方声明,但是只在一个地方定义。
使用extern关键字可以声明一个已经在其他文件或作用域中定义的变量。这种声明告诉编译器该变量的存在,但实际上并没有定义它(即不分配内存),变量的实际定义必须在某个地方单独完成。
深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间进行拷贝操作
浅拷贝(shallow copy)主要是对指针的拷贝,而不是对指针所指向的内容进行拷贝。这意味着,如果原对象中包含指向其他对象的指针,那么这些指针在新创建的对象中仍然指向原来的对象。因此,当原对象被销毁时,这些指针可能会导致未定义的行为,如悬挂指针问题。浅拷贝可以理解为"值"层面的拷贝。
深拷贝(deepcopy),则是完全复制了父对象及其子对象。这包括了对指针所指向的内容的完全复制,从而确保了即使原对象被销毁,新创建的对象也能独立存在。深拷贝可以避免悬挂指针的问题,并且当修改副本时,不会影响原对象。深拷贝可以理解成"内存"上的拷贝。
总结来说,浅拷贝和深拷贝的主要区别在于是否对指针所指向的内容进行了复制。浅拷贝只复制了指针本身,而深拷贝则复制了指针及其指向的内容。在实际使用中,选择哪种拷贝方式取决于具体的需求和场景。例如,当需要确保复制后对象的独立性,避免因原对象变化而导致的潜在问题时,应选择深拷贝。
C++内存四大区域
C++ 内存可以分为四个区域:静态存储区和动态存储区(又分为栈区和堆区)和代码区。具体介绍如下:
- 代码区:存放函数体的二进制代码,由操作系统进行管理。代码区是共享的,多次执行该程序,内存中也只需有一份代码。代码区是只读的,避免程序意外修改指令。
- 全局区:存放全局变量、静态变量以及常量。程序执行后由操作系统自动释放。
- 栈区:由编译器自动分配释放,存放函数的参数值、局部变量等。
- 堆区:由程序员分配和释放,若程序员不释放,程序结束时由系统回收。
静态库与动态库的区别
1.链接方式:静态库在程序编译时被直接链接到可执行文件中,这意味着所有的库代码都会被包含在最终的可执行文件中。相比之下,动态库(也称为共享库或DLL)在程序运行时才被加载,不会被包含在可执行文件本身中。
2.空间占用:由于静态库的所有代码都被包含在可执行文件中,因此生成的可执行文件体积较大。而动态库只包含对库的引用和接口信息,实际的库代码是独立存储的,这样可以减少可执行文件的大小,同时提高系统的效率和资源利用率。
3.使用场景:静态库适用于那些需要快速部署且不希望依赖外部库的场景。总结来说,选择静态库还是动态库取决于具体的应用需求和环境条件。如果项目对启动时间和内存占用有严格要求,或者需要快速部署,那么静态库可能是更好的选择。相反,如果项目需要高度的模块化和灵活性,以便于未来的维护和升级,那么动态库将是更合适的选择
什么时候使用静态库什么时候使用动态库
在决定使用C++的静态库还是动态库时,需要考虑几个关键因素。
首先,静态库将库代码静态地编译到可执行文件中,这使得可执行文件的大小会增大。相比之下,动态库在运行时从共享库中加载所需的代码,因此可执行文件的大小较小。这意味着,如果对程序大小有严格限制,或者希望减少最终用户的安装负担,动态库可能是更好的选择。
其次,静态库一旦被链接,其中所有的代码都不再发生更改;而动态库可以在程序运行时被升级或替换。这表明,如果需要在不重新编译整个程序的情况下更新或修复库中的错误,动态库提供了更大的灵活性。
此外,静态库适用于需要保证稳定性和安全性的程序,例如系统程序。这是因为静态链接可以确保所有必要的代码都包含在最终的可执行文件中,减少了运行时依赖的风险。相反,动态库适用于需要共享代码的程序,例如开发工具包。这种共享机制允许多个程序共享相同的代码库,从而节省存储空间并提高代码的复用性。
综上所述,选择使用静态库还是动态库主要取决于以下几个因素:
1.程序大小和用户安装负担:如果对程序大小有限制或希望减少用户的安装负担,应考虑使用动态库。
2.更新和维护的便利性:如果需要在不重新编译整个程序的情况下更新或修复库中的错误,动态库提供了更大的灵活性。
3.稳定性和安全性需求:对于需要高稳定性和安全性的系统程序,静态库可能是更合适的选择。
4.代码复用性和共享:如果目标是提高代码的复用性和共享性,动态库提供了这样的机制。
对C++11的了解(简洁)
统一初始化语法(也叫做大括号初始化):包括初始化列表、统一的初始化语法等,这些改进使得C++的使用更加直观和方便。例:std::vector v{2, 3, 5, 7,};
基于范围的for循环:简化了容器遍历的语法,使得遍历变得更加简洁易懂。
auto关键字:在 C++11 中,auto关键字表示自动推导类型。它允许变量的类型由编译器推断出来,而不需要显式地指定类型。这样可以简化代码,提高编程效率。例如:auto x = 42;,x的类型会被推断为int。
lambda表达式:lambda表达式是一个匿名函数,可以在代码中直接定义和使用。它的出现使得 C++的函数式编程更加方便和灵活。例如:auto sum = [](int x, int y) { return x + y; };,这里定义了一个lambda表达式,它接受两个int类型的参数并返回它们的和。
array数组:array是 C++11 引入的一种固定长度的数组容器,它的效率与普通数组相同,但提供了更多的功能和便利。例如:array<int, 10> arr = {1, 2, 3, 4, 5};,这里定义了一个包含10个int类型元素的数组arr。
nullptr关键字:nullptr是 C++11 引入的一个空指针常量,用于表示空指针。它的引入是为了解决 C++中的NULL的二义性问题。NULL可以表示0,也可以表示空指针,而nullptr则专门用于表示空指针。例如:void fun(int *p) { assert(p != nullptr); cout << *p << endl; },这里使用nullptr来表示空指针,而不是NULL。提高了代码的安全性和可读性。
智能指针:std::shared_ptr和std::unique_ptr是 C++11 中新增的两个智能指针,可以帮助程序员更轻松地管理内存。它们能够自动释放所管理的动态分配的内存,防止内存泄漏和野指针等问题。
thread库:
1.简化线程创建和管理:通过std::thread类,可以很容易地创建一个新的执行线程。只需将希望在新线程中运行的函数或可调用对象传递给std::thread的构造函数即可。
2.支持线程同步:库还提供了互斥锁(std::mutex)和条件变量(std::condition_variable),这些同步原语可以帮助管理线程间的共享资源访问,防止数据竞争和实现线程间的正确协作。
3.提供异步编程接口:C++11还引入了std::async,这是一个高级别的异步编程接口,它允许以非阻塞的方式启动一个任务,并在后台自动创建一个线程来执行该任务。这简化了异步任务的管理,并且在某些情况下,可以提高程序的性能。
C++智能指针
四个智能指针以及原理:
auto_ptr是一种独占所有权的智能指针,它的特点是在任何时候都只有一个auto_ptr指向一个给定的资源。当auto_ptr被销毁或者重新赋值时,它所指向的资源也会被自动释放。但是,由于auto_ptr在拷贝操作中会转移所有权,这可能导致一些意想不到的问题,因此C++11将其废弃,并推荐使用unique_ptr来代替。
unique_ptr也是独占所有权的智能指针,它通过禁止拷贝构造函数来保证同一时间只有一个unique_ptr指向同一个资源。这种设计可以有效防止多个指针同时拥有资源的所有权,从而避免了资源的重复释放。
shared_ptr是一种共享所有权的智能指针,它允许多个shared_ptr实例指向同一个资源。shared_ptr内部使用引用计数来跟踪有多少个shared_ptr实例指向同一个资源,只有当最后一个shared_ptr被销毁时,资源才会被自动释放。这种智能指针适用于多个对象需要共享同一个资源的情况。
weak_ptr是一种不拥有所有权的智能指针,它主要用于解决shared_ptr循环引用导致的死锁问题。weak_ptr允许在不增加引用计数的情况下观察一个shared_ptr所管理的对象,这样可以避免循环引用导致的资源无法释放的问题。
智能指针的实现原理主要基于RAII(Resource Acquisition Is Initialization)的概念,即在对象构造时获取资源,并在对象的生命周期结束时释放资源。这种方法可以有效地管理动态分配的内存,减少内存泄露的风险,并简化内存管理的工作。
智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,析构函数会自动释放资源。
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() { std::cout << "MyClass constructor" << std::endl; }
~MyClass() { std::cout << "MyClass destructor" << std::endl; }
};
int main() {
// 使用 unique_ptr 管理动态分配的内存
std::unique_ptr<MyClass> uptr(new MyClass());
// 当 uptr 离开其作用域时,MyClass 的析构函数将被调用,资源将被释放
// 使用 shared_ptr 管理动态分配的内存
std::shared_ptr<MyClass> sptr = std::make_shared<MyClass>();
// 多个 shared_ptr 可以共享同一个资源
std::shared_ptr<MyClass> sptr2 = sptr;
// 当 sptr 和 sptr2 都离开其作用域时,MyClass 的析构函数将被调用,资源将被释放
// 使用 weak_ptr 观察 shared_ptr 所管理的对象
std::weak_ptr<MyClass> wptr(sptr);
// wptr 不会改变 sptr 的引用计数
return 0;
}
智能指针的选择
std::unique_ptr:当需要独占所有权时,应使用std::unique_ptr。这表示在同一时间只有一个unique_ptr可以指向一个给定的资源。这种智能指针适用于单拥有者的场景,确保资源在不再需要时被释放。unique_ptr提供了独占所有权的语义,避免了多个指针同时拥有资源的所有权,从而防止了潜在的资源重复释放问题。
std::shared_ptr:如果多个指针需要共享同一个资源,那么应该使用std::shared_ptr。它通过引用计数来实现共享所有权,确保资源在所有shared_ptr都不再需要时被释放。这种智能指针适用于多个对象需要访问同一个资源的情况。
std::weak_ptr:为了避免shared_ptr的循环引用问题,可以使用std::weak_ptr。它允许在不增加引用计数的情况下观察一个shared_ptr所管理的对象,这样可以避免因循环引用导致的资源无法释放。weak_ptr通常与shared_ptr一起使用,以解决特定的生命周期管理问题。
std::auto_ptr:在C++11及以后的标准中,std::auto_ptr已被弃用,并由std::unique_ptr替代。因此,不建议使用auto_ptr,而应该使用unique_ptr作为替代方案。
C++线程与进程
创建线程
#include <iostream>
#include <thread>
void print_hello() {
std::cout << "Hello from thread!" << std::endl;
}
int main() {
// 创建一个线程对象,传入要执行的函数
std::thread t(print_hello);
// 等待线程执行完成
t.join();
return 0;
}
线程库
背景:
在C++11之前,创建线程通常需要依赖于平台相关的API或者第三方库。具体如下:
平台相关API:在Windows上,可以使用Windows API中的CreateThread函数来创建线程。而在类UNIX系统(如Linux)上,通常会使用POSIX线程(pthread)库中的pthread_create函数来创建线程。
第三方库:例如Boost库,它提供了一个跨平台的线程库,可以在C++11之前的版本中使用。
在C++11之前,由于没有统一的标准,开发者需要根据不同的平台选择合适的线程创建方式,这增加了代码的复杂性和移植难度。而且,这些方法通常不如C++11的std::thread那样简洁和易于使用。
总的来说,C++11通过引入std::thread为线程编程带来了标准化和简化,使得多线程编程更加方便和高效。
进程与线程的区别
独立性:
进程:每个进程都有自己独立的地址空间,每启动一个进程,系统都会为其分配内存、I/O 等资源。
线程:同一进程内的多个线程共享该进程的地址空间和其他资源。
资源开销:
进程:创建进程时,需要为其分配独立的资源,包括内存、文件句柄等,因此开销较大。
线程:线程可以更快地创建和切换,因为它们共享进程的资源,不需要为其分配独立的内存空间。
生命周期:
进程:进程的生命周期与程序的运行直接相关,程序结束时,所有进程都会被销毁。
线程:线程的生命周期由它自己的运行情况决定,主线程结束时,子线程可能还会继续运行。
调度单位:
进程:进程是资源分配和调度的基本单位。
线程:线程是程序执行的最小单位,它是CPU调度和分派的基本单位。
独立性和影响范围:
当一个进程崩溃时,它不会影响到其他进程,因为每个进程都有自己的资源空间。相反,如果一个线程崩溃或出现错误,它将直接影响到所在的整个进程,因为所有线程都共享该进程的地址空间。
什么时候用进程,什么时候用线程
并行性:
如果你的应用需要执行多个独立的任务,且这些任务之间不需要共享状态或数据,那么可以考虑使用多进程。
如果你的应用需要执行多个任务,并且这些任务需要共享数据或状态,或者需要更快的创建和上下文切换,那么应该使用多线程。
资源隔离:
如果需要更强的资源隔离(如内存、文件描述符等),或者需要进行安全性考虑(如防止一个任务影响其他任务),则使用进程更为合适。
如果任务之间需要共享资源(如内存、文件等),则使用线程更加方便。
通信机制:
如果任务之间需要复杂的通信机制,如消息队列、管道等,可能更适合使用进程。
如果任务之间需要简单快速的通信,可以直接通过共享内存进行,那么线程是更好的选择。
系统开销:
创建进程通常比创建线程有更大的系统开销,因为进程拥有独立的地址空间和其他资源。
线程共享进程的资源,因此创建和切换线程的开销较小。
易用性和复杂性:
使用多线程编程通常比多进程编程更简单,因为线程可以直接访问共享数据。
进程间通信和同步通常需要更多的代码和机制。
进程间通信(IPC)
管道(Pipe):管道是半双工的通信方式,通常用于具有亲缘关系的进程间通信,比如父子进程。它允许一个进程向另一个与它有共同祖先的进程发送信息。
命名管道(Named Pipe):也称为FIFO(First In First Out),是一种全双工的通信方式,可用于无亲缘关系的进程间通信。命名管道在文件系统中有对应的名称,因此任何知道该名称的进程都可以与之通信。
共享内存(Shared Memory):共享内存是一种高效的IPC方式,它允许多个进程访问同一块内存区域。这种方式通常用于大量数据的传输,因为它避免了数据复制的开销。
信号(Signal):信号是一种异步通信机制,用于通知接收进程某个事件已经发生。它可以用于进程间的通知,但传递的信息量有限。
消息队列(Message Queue):消息队列允许进程之间发送格式化的消息,这些消息可以被存储在内核中,直到被目标进程读取。
套接字(Socket):套接字是一种网络通信方式,可以用于同一台机器上的进程间通信,也可以用于不同机器之间的通信。
信号量(Semaphores):信号量用于进程间的同步,确保资源在同一时间只被一个进程访问。
?有没有用过哪些进程同步(详细):
线程间通信
互斥量(Mutex):互斥量是一种同步原语,用于保护共享资源,防止多个线程同时访问。当一个线程需要访问共享资源时,它会尝试获取互斥量的锁。如果锁已经被其他线程持有,该线程将阻塞,直到锁被释放。
条件变量(Condition Variable):条件变量用于线程之间的等待和通知机制。一个线程可以等待某个条件成立,而另一个线程可以在条件成立时通知等待的线程。这通常与互斥量一起使用,以确保线程在等待和通知时不会发生竞争条件。
信号量(Semaphore):信号量是一种计数器,用于控制对共享资源的访问。线程可以通过增加或减少信号量的值来请求或释放资源。当信号量的值为0时,线程将被阻塞,直到其他线程释放资源。
事件(Event):事件是一种同步原语,用于通知一个或多个线程某个事件已经发生。一个线程可以等待事件的发生,而另一个线程可以在事件发生时设置事件。这通常与互斥量一起使用,以确保线程在等待和设置事件时不会发生竞争条件。
消息队列(Message Queue):消息队列是一种用于线程间通信的数据结构,允许线程发送和接收消息。一个线程可以将消息放入队列,而另一个线程可以从队列中取出消息进行处理。这通常与互斥量和条件变量一起使用,以确保线程在发送和接收消息时不会发生竞争条件。
?线程间同步
STL容器
什么情况下用vector,什么情况下用list,什么情况下用deque
使用vector的情况:
当你需要随机访问元素时,因为vector提供了快速的随机访问能力。
当你需要连续存储数据时,因为vector内部是连续存储的。
当你需要频繁进行尾部插入和删除操作时,因为vector的尾部插入和删除效率较高。
使用list的情况:
非连续的存储结构,适用于需要频繁插入/删除操作且不关心随机访问性能的场景
使用deque的情况:
连续的存储结构,适合需要频繁在序列两端进行插入和删除,并且需要快速随机访问的场景
vector的原理,为什么1.5或2扩容
vector的底层实现原理主要包括以下几个方面:
连续内存分配:vector在底层使用一个连续的内存块来存储元素,这意味着可以通过下标快速访问元素,时间复杂度为O(1)。
自动扩容:当元素数量超过vector的当前容量时,vector会自动进行扩容。这个过程中,会重新分配一个更大的连续内存空间,并将原有元素复制到新的内存空间中。
预留空间:为了优化性能,减少频繁的内存重新分配操作,vector在每次扩容时会预留一些额外的空间,这样在未来增长需求出现时可以不必立即再次扩容。
vector的扩容机制是通过在需要时增加其容量来避免频繁的内存重新分配,通常情况下,当vector的当前容量不足以容纳新添加的元素时,它的容量会以一个特定的倍数增长,这个倍数通常是1.5或2倍。
具体来说,vector的扩容机制涉及到以下几个关键点:
- 空间判断:当向vector中添加元素时,如果当前已用空间已经达到了总容量,就需要进行扩容操作。
- 新空间申请:在扩容过程中,会申请一个新的内存块,其大小通常是原空间大小的1.5倍或2倍。
- 元素转移:将原内存块中的元素复制到新的内存空间中。
- 释放旧空间:完成元素复制后,释放原内存块以回收资源。
- 更新指针:使vector指向新的内存空间。
选择1.5或2倍的原因主要是为了平衡内存利用率和元素的复制成本。太小的扩容倍数会导致频繁的扩容操作,而太大的扩容倍数则可能导致过多的空闲内存。通过选择一个适中的倍数,可以在避免频繁扩容的同时,也不至于造成太多浪费的内存空间。
此外,不同的操作系统可能会选择不同的扩容策略。例如,Windows可能使用1.5倍的扩容策略,而Linux可能使用2倍的扩容策略。这些策略的选择取决于操作系统的设计决策和对性能的考量。
在实际使用中,可以通过预留足够的容量或者使用reserve
函数来减少vector的扩容次数,从而提高性能。
vector与set的区别
内部实现
vector:使用连续的内存空间,类似于动态数组。当插入新元素超过预留空间时,可能需要重新分配内存并移动所有元素,这会导致较高的插入成本。
set:底层通常使用红黑树实现,存储元素时自动进行排序,且所有元素都是唯一的。由于树结构的特性,插入和删除操作相对高效。
元素特性
vector:可以包含重复的元素,并且元素不进行自动排序。适合需要按索引快速访问元素的场景。
set:所有元素都是唯一的,且自动按照顺序排序。适合需要快速查找和插入的场景,但不支持通过索引访问元素。
随机访问
vector:支持随机访问,即可以通过下标迅速访问任何元素。
set:不支持随机访问,只能通过迭代器进行遍历。
插入删除效率
vector:在末尾插入和删除元素的速度非常快,但在中间或开头插入和删除效率较低,因为需要移动元素。
set:在任何位置插入和删除的效率都较高,因为只需调整树结构而无需移动大量元素。
map与unordered_map的区别
底层实现:map是基于红黑树实现的,这意味着它在内部保持了元素的有序状态。而unordered_map则是基于哈希表实现的,不保持元素有序。
查找效率:在不需要有序遍历的情况下,unordered_map提供更快的查找效率。但是,如果需要有序地访问元素,map将是更好的选择,因为它维持了元素的排序顺序。
插入效率:unordered_map的插入效率更高
内存占用:unordered_map可能会比map占用更多的内存,因为哈希表的实现通常需要额外的空间来存储哈希桶等信息。
总的来说,当需要有序访问或者保持元素插入顺序时,应优先选择map;而在查找速度至关重要且不关心元素顺序的场景下,unordered_map会是更佳的选择。
map与multimap的区别
首先,从对应关系上看,map容器中每个键只能对应一个值,它保证了键的唯一性。而multimap则允许一个键对应多个值,这适用于需要存储一对多关系的数据场景。
其次,从访问操作上看,由于map中的键是唯一的,可以使用[]操作符直接通过键来访问或修改对应的值。但在multimap中,由于一个键可能对应多个值,所以不支持[]操作符,而是需要使用迭代器或其他方法来访问具有相同键的所有值。
总结来说,map和multimap虽然都是用于存储键值对的关联容器,但map保证键的唯一性并支持[]操作符,而multimap允许键的重复并使用迭代器进行访问。在选择使用哪种容器时,应根据具体需求来决定。
map与set的区别
两者都是基于红黑树实现的
关于元素唯一性,set的特点是所有存储在其中的元素都是唯一的,这使它非常适合用于去重和集合操作。相比之下,map由于其键值对的结构,不仅键是唯一的,而且可以通过键来快速查找对应的值,这在需要快速检索的场景下非常有用。
从主要用途上来说,map通常用于需要快速查找数据的情况,例如实现字典或数据库索引等。而set则适用于需要确保元素唯一性的场景,如去除列表中的重复项或测试某个元素是否存在于集合中。
总的来说,map提供了一种通过键来快速访问值的方式,而set则提供了一种存储唯一元素的方法。在实际编程中,选择使用map还是set取决于具体的应用场景和需求。
仿函数的概念
C++中的仿函数(Functor)是一种设计模式,允许将一个类的对象用作一个函数。具体来说,仿函数是一个重载了圆括号运算符operator()的类或结构体,这使得它的实例可以像普通函数那样被调用。仿函数不仅能够接受参数、返回值,还可以拥有自己的状态,这使得它们比普通函数更加灵活,能够处理更复杂的逻辑。
如何调试程序(找BUG)
Windbg:
准备工作:
1.dump文件
2.源代码(必须与编译可执行文件时的代码一致)
3.pdb文件(一定要与可执行文件同时生成的,即使源代码一致,重新生成的pdb文件也不行)
使用过程:
左上角File->Source File Path (输入源代码的路径)
左上角File->Symbol FilePath(输入pdb文件的路径)
左上角File->Open Crash Dump(打开dump文件)
命令:
.reload 重新加载pdb文件
!analyze -v 分析dump文件
kbn 显示当前线程调用栈信息
.ecxr打开栈顶关联的源码
~*kbn 显示所有线程信息
vs调试dump 参考
少见
C与C++的区别
语法和特性:C++是在C语言的基础上进行扩展而来的,因此C++继承了C语言的语法,并且增加了更多的特性,如类、对象、继承、多态等。C++还引入了一些新的关键字和语法规则,如命名空间、模板、异常处理等。
面向对象编程:C++是一种支持面向对象编程(OOP)的语言,而C语言则不支持。面向对象编程可以更好地组织和管理代码,提高代码的可重用性和可维护性。
标准库:C++标准库相对于C语言的标准库更加丰富和强大。C++标准库提供了大量的容器类、算法、迭代器等,可以方便地进行各种操作,如字符串处理、文件操作、数据结构等。而C语言的标准库相对较少,主要包括一些基本的输入输出函数和数学函数。
内存管理:C++相比C语言更加注重内存管理。C++引入了构造函数和析构函数的概念,可以自动管理对象的创建和销毁。此外,C++还提供了new和delete运算符,用于动态分配和释放内存。
命名空间:为了避免名称冲突,C++引入了命名空间的概念,允许程序员将一组标识符封装在一个命名空间内。
数据结构
常见的数据结构有哪些
C++中常见的数据结构有以下几种:
- 数组(Array):这是一种基础的数据结构,它允许存储固定大小的同类型元素集合。数组的内存是连续分配的,可以通过下标索引来访问各个元素。
- 栈(Stack):栈是一种遵循后进先出(LIFO)原则的线性数据结构。在栈中,元素的添加和删除都发生在同一端,即栈顶。
- 队列(Queue):队列是一种遵循先进先出(FIFO)原则的线性数据结构。在队列中,元素从一端添加,从另一端删除。
- 链表(Linked List):链表是一种非线性数据结构,由一系列节点组成,每个节点包含数据和指向下一个节点的指针。链表可以有单链表、双链表等形式。
- 树(Tree):树是一种层次型的数据结构,由根节点和子树构成。二叉树是一种特殊的树结构,每个节点最多有两个子节点。
- 图(Graph):图是由节点(顶点)和连接节点的边组成的复杂数据结构。图可以是无向的或有向的,也可以是加权的。
- 堆(Heap):堆是一种特殊的完全二叉树结构,通常用于实现优先队列。在堆中,父节点的值总是大于(最大堆)或小于(最小堆)其子节点的值。
- 散列表(Hash Table):散列表也称为哈希表,是一种通过哈希函数组织数据,以实现快速查找、插入和删除操作的数据结构。
红黑树的理解
红黑树是一种自平衡二叉查找树。
红黑树是在计算机科学中广泛应用的一种数据结构,在C++STL中的set、multiset、map和multimap等关联容器的实现基础上就有红黑树的身影。以下是它的一些关键特性:
- 节点颜色:每个节点都有一个颜色属性,要么是红色,要么是黑色。
- 根节点颜色:红黑树的根节点总是黑色的。
- 叶子节点颜色:所有的叶子节点(NIL节点)都是黑色。在这里,叶子节点指的是那些没有子节点的空节点。
- 路径黑节点数:从根到叶子的任意路径上包含相同数目的黑节点,这一性质确保了树的平衡性。
- 最长与最短路:任何一条从根到叶子的路径,其最长和最短的长度不会超过两倍之差,即最长路径是最短路径长度的2倍。
- 子节点颜色:如果一个节点是红色的,则它的子节点必须是黑色的。这个规则避免了红色节点的相邻出现。
设计模式
工厂模式
简单工厂模式、工厂模式和抽象工厂模式各自适用于不同的场景:
- 简单工厂模式适用于产品种类较少且需求稳定的场景。它通过一个中心化的工厂类来创建对象,客户端只需传入参数,工厂类会根据参数来决定创建哪种产品类的实例。这种方式简化了对象的创建过程,但缺点是当产品种类增多时,工厂类的代码会变得复杂且难以管理。
- 工厂模式适用于产品种类较多且需要对扩展开放的场景。在工厂模式中,创建对象的任务被交给了子类,每个子类可以实现自己的工厂方法来生成具体的产品。这样做的好处是增加了系统的灵活性,当需要增加新的产品时,只需要增加相应的具体工厂类即可。
- 抽象工厂模式适用于产品族系列较多且产品等级结构较复杂的场景。抽象工厂模式提供了一种接口用于创建一系列相关或相互依赖的对象,而无需指定它们具体的类。这种模式主要用于创建复杂的产品族,当一个产品族中的多个产品之间存在依赖关系时,抽象工厂模式可以确保只产生一组一致的产品。
总的来说,简单工厂模式适合于产品较少且不经常变动的情况;工厂模式适用于产品较多且需要易于扩展的情况;抽象工厂模式则适用于创建一系列相关或相互依赖的对象的复杂情况。在实际应用中,应根据具体需求选择合适的设计模式以实现最佳的软件设计和架构。
简单工厂模式:
#include <iostream>
using namespace std;
class Product {
public:
virtual void show() = 0;
};
class ProductA : public Product {
public:
void show() {
cout << "Product A" << endl;
}
};
class ProductB : public Product {
public:
void show() {
cout << "Product B" << endl;
}
};
class SimpleFactory {
public:
static Product* createProduct(char type) {
switch (type) {
case 'A': return new ProductA();
case 'B': return new ProductB();
default: return nullptr;
}
}
};
int main() {
Product* productA = SimpleFactory::createProduct('A');
if (productA != nullptr) {
productA->show();
delete productA;
}
Product* productB = SimpleFactory::createProduct('B');
if (productB != nullptr) {
productB->show();
delete productB;
}
return 0;
}
工厂模式:
#include <iostream>
using namespace std;
class Product {
public:
virtual void show() = 0;
};
class ProductA : public Product {
public:
void show() {
cout << "Product A" << endl;
}
};
class ProductB : public Product {
public:
void show() {
cout << "Product B" << endl;
}
};
class Factory {
public:
virtual Product* createProduct() = 0;
};
class FactoryA : public Factory {
public:
Product* createProduct() {
return new ProductA();
}
};
class FactoryB : public Factory {
public:
Product* createProduct() {
return new ProductB();
}
};
int main() {
Factory* factoryA = new FactoryA();
Product* productA = factoryA->createProduct();
productA->show();
delete productA;
delete factoryA;
Factory* factoryB = new FactoryB();
Product* productB = factoryB->createProduct();
productB->show();
delete productB;
delete factoryB;
return 0;
}
抽象工厂模式:
#include <iostream>
using namespace std;
class ProductA {
public:
void show() {
cout << "Product A" << endl;
}
};
class ProductB {
public:
void show() {
cout << "Product B" << endl;
}
};
class AbstractFactory {
public:
virtual ProductA* createProductA() = 0;
virtual ProductB* createProductB() = 0;
};
class ConcreteFactory1 : public AbstractFactory {
public:
ProductA* createProductA() {
return new ProductA();
}
ProductB* createProductB() {
return new ProductB();
}
};
class ConcreteFactory2 : public AbstractFactory {
public:
ProductA* createProductA() {
return new ProductA();
}
ProductB* createProductB() {
return new ProductB();
}
};
int main() {
AbstractFactory* factory1 = new ConcreteFactory1();
ProductA* productA1 = factory1->createProductA();
productA1->show();
delete productA1;
ProductB* productB1 = factory1->createProductB();
productB1->show();
delete productB1;
delete factory1;
AbstractFactory* factory2 = new ConcreteFactory2();
ProductA* productA2 = factory2->createProductA();
productA2->show();
delete productA2;
ProductB* productB2 = factory2->createProductB();
productB2->show();
delete productB2;
delete factory2;
return 0;
}
观察者模式
参考
观察者模式是一种行为型设计模式,它定义了对象之间的一对多依赖关系,当一个对象(通常称为“主题”或“可观察对象”)的状态发生改变时,所有依赖于它的对象(称为“观察者”)都会得到通知并自动更新。
以下是一个简单的C++实现:
#include <iostream>
#include <vector>
// 抽象观察者类
class Observer {
public:
virtual void update() = 0;
};
// 具体观察者类
class ConcreteObserver : public Observer {
public:
void update() override {
std::cout << "ConcreteObserver: Received update notification." << std::endl;
}
};
// 抽象主题类
class Subject {
public:
virtual void addObserver(Observer* observer) = 0;
virtual void removeObserver(Observer* observer) = 0;
virtual void notifyObservers() = 0;
};
// 具体主题类
class ConcreteSubject : public Subject {
private:
std::vector<Observer*> observers;
public:
void addObserver(Observer* observer) override {
observers.push_back(observer);
}
void removeObserver(Observer* observer) override {
observers.erase(std::remove(observers.begin(), observers.end(), observer), observers.end());
}
void notifyObservers() override {
for (Observer* observer : observers) {
observer->update();
}
}
};
int main() {
ConcreteSubject subject;
ConcreteObserver observer1, observer2, observer3;
subject.addObserver(&observer1);
subject.addObserver(&observer2);
subject.addObserver(&observer3);
subject.notifyObservers();
return 0;
}
在这个例子中,ConcreteSubject
是具体主题类,它维护了一个观察者列表。当主题的状态发生变化时,它会调用notifyObservers()
方法通知所有的观察者。ConcreteObserver
是具体观察者类,它实现了Observer
接口的update()
方法,用于接收主题的通知。在main()
函数中,我们创建了一个主题和三个观察者,并将观察者添加到主题的观察者列表中。最后,我们调用subject.notifyObservers()
来模拟主题状态的变化,从而触发观察者的更新操作。
单例模式
一个类只允许创建一个实例对象,并提供访问其唯一的对象的方式。这个类就是一个单例类,这种设计模式叫作单例模式。
C++单例模式的实现方式主要有以下几种:
- 懒汉式:在第一次调用时实例化对象,但没有考虑多线程环境,可能会创建多个实例。
- 饿汉式:在类加载时就完成了实例化,因此不存在线程同步问题。
- 双重检查锁定(DCL):这种方式结合了懒汉式的延迟初始化和多线程环境下的线程安全。
以下是这些实现方式的代码示例:
- 懒汉式:
class Singleton {
private:
static Singleton *instance;
Singleton() {}
public:
static Singleton* getInstance() {
if (instance == NULL) {
instance = new Singleton();
}
return instance;
}
};
Singleton* Singleton::instance = NULL;
- 饿汉式:
class Singleton {
private:
static Singleton instance;
Singleton() {}
public:
static Singleton& getInstance() {
return instance;
}
};
Singleton Singleton::instance;
- 双重检查锁定(DCL):
class Singleton {
private:
static Singleton* instance;
static std::mutex mtx;
Singleton() {}
public:
static Singleton* getInstance() {
if (instance == NULL) {
std::lock_guard<std::mutex> lock(mtx);
if (instance == NULL) {
instance = new Singleton();
}
}
return instance;
}
};
Singleton* Singleton::instance = NULL;
std::mutex Singleton::mtx;
C++中懒汉式(Lazy Initialization)和饿汉式(Eager Initialization)是两种实现单例模式的方法,它们的主要区别在于单例对象的创建时机和线程安全性的表现:
懒汉式(Lazy Initialization):
- 概念:懒汉式会在第一次真正需要使用单例对象时才去创建它,即延迟初始化。这意味着直到客户端第一次调用获取单例对象的方法时,单例才会被实例化。
- 示例代码(简单非线程安全版本):
class Singleton { private: static Singleton* _instance; Singleton() {} // 私有构造函数 public: static Singleton* getInstance() { if (_instance == nullptr) { _instance = new Singleton(); } return _instance; } // 必须禁止拷贝构造和赋值操作,防止通过复制单例对象破坏单例性质 Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; }; Singleton* Singleton::_instance = nullptr;
- 线程安全问题:在多线程环境下,如果多个线程同时检测到_instance为空并尝试创建实例,会导致多个实例被创建。为此,懒汉式需要额外的同步机制(如互斥锁)来保证线程安全。
饿汉式(Eager Initialization):
- 概念:饿汉式在程序加载或类初始化阶段就已经完成了单例对象的实例化,因此在任何时刻只要需要获取单例对象,它总是可用的。
- 示例代码(线程安全):
class Singleton { private: Singleton() {} // 私有构造函数 static Singleton _instance; // 静态成员变量,编译器保证其初始化时的线程安全性 public: static Singleton& getInstance() { return _instance; } Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; }; // 初始化实例 Singleton Singleton::_instance;
- 线程安全:饿汉式通常天然地具备线程安全性,因为它依赖于C++静态变量初始化的线程安全性,即在类加载时创建单例对象。
总结:
- 懒汉式在第一次调用时才实例化,适合内存有限或单例不一定会被使用的场景,但需要额外的同步手段来确保线程安全。
- 饿汉式在类加载时就实例化,无需担心线程安全问题,但是不管单例对象是否会被使用都会占用内存。在多线程环境下,尤其在不需要考虑过多内存开销且单例必然被使用的场景下,饿汉式更为简单和高效。
网络协议
TCP与UDP的区别
TCP是面向连接的,UDP是无连接的;
TCP是可靠的,UDP是不可靠的;
TCP是面向字节流的,UDP是面向数据报文的;
TCP只支持点对点通信,UDP支持一对一,一对多,多对多;
TCP协议三次握手
TCP的三次握手是建立TCP连接的过程,具体包括以下步骤:
第一次握手:客户端发送一个SYN(同步序列编号)标志的数据包到服务器,以开始建立连接。这个数据包还包含一个随机的序列号X,用于后续的数据传输同步。发送完成后,客户端进入SYN_SEND状态。
第二次握手:服务器收到客户端的SYN包后,会返回一个SYN/ACK(同步/应答)标志的数据包,其中SYN表示同意建立连接,ACK是对客户端SYN包的确认。这个数据包也会包含一个随机的序列号Y,以及确认号(X+1),表示已经收到了客户端的SYN包。此时,服务器进入SYN_RECV状态。
第三次握手:客户端收到服务器的SYN/ACK包后,会发送一个ACK标志的数据包,包含自己的序列号(X+1)和确认号(Y+1),表示已经准备好进行数据传输。当服务器收到这个ACK包后,连接就正式建立了。此时,双方均进入ESTABLISHED状态。
这个过程主要是为了防止已失效的连接请求报文段突然传送到服务器,从而产生错误。通过这三次交互,客户端和服务器都能确认对方具备接收和发送数据的能力,确保了连接的可靠性。
HTTP协议
HTTP协议,全称为超文本传输协议(HyperText Transfer Protocol),是用于在互联网上进行数据通信的应用层协议。它基于TCP/IP协议工作,确保了客户端与服务器之间的可靠传输。
头文件问题
#include < >:只搜索系统目录,不会搜索本地目录
#include " ":首先搜索本地目录,若找不到才会搜索系统目录
函数传参的三种方式:值传递,地址传递,引用传递
如何选择:参考
为什么函数参数常用引用而不用指针
安全性:
- 引用在定义时必须被初始化,并且一旦被初始化后就不能改变绑定的对象。这减少了空引用的风险,而指针可以被初始化为
nullptr
,或者在运行时指向无效的内存,导致程序崩溃。- 引用不存在悬空引用的问题,因为引用总是连接到一个有效的对象。而指针如果管理不当,可能会变成野指针或悬挂指针,指向已经被释放的内存。
易用性:
- 引用的使用更像普通的变量,无需解引用操作符(如
*
),使得代码更简洁、易读。- 通过引用传递大型对象时,可以直接操作对象本身,而不需要通过指针间接操作,这使得代码看起来更像是直接操作原始对象。
效率:
- 虽然在大多数现代编译器中,指针和引用的性能差异微乎其微,但引用避免了潜在的解引用操作,可能在某些情况下略微提高效率。
- 引用不会占用额外的存储空间来保存地址,它本质上是一个别名,不像指针那样需要存储对象的地址。
语义清晰
- 使用引用传递参数,特别是在表示“按引用传递”时,更直观地表达了意图,即函数可能会修改传入的对象。
- 对于const引用,还能表达出“按常量引用传递”,即函数不会修改参数,但可以避免复制,这对于大型对象尤其有用。
编译时检查:
- 引用的类型检查更为严格,如果函数期望一个特定类型的引用,编译器会在编译时强制类型匹配,减少类型错误。
综上所述,虽然指针在某些特定场景下(如需要动态改变指向或实现复杂的数据结构)依然有其必要性,但在许多常规的函数参数传递场景中,引用因其更高的安全性和便利性而成为首选。
如何避免“野指针”
指针变量声明时没有被初始化。解决办法:指针声明时初始化,可以是具体的地址值,也可让它指向NULL。
指针p被free或者delete之后,没有置为NULL。解决办法:指针指向的内存空间被释放后指针应该指向NULL。
合理使用智能指针:C++11引入了智能指针,如std::unique_ptr和std::shared_ptr,它们可以自动管理内存,当对象不再使用时自动释放内存,从而减少野指针的产生。
排序算法
快速排序的原理
1.选择基准元素:从数组中选择一个元素作为基准(pivot),这个基准元素的位置将用于之后的分区操作。
2.分区操作:重新排列数组中的元素,所有比基准小的元素摆放在基准前面,所有比基准大的元素摆放在基准后面。在这个操作结束后,基准就处于数组的中间位置,这个称为分区(partition)操作。
3.递归排序子数组:递归地将小于基准的子数组和大于基准的子数组进行快速排序。
const 使用
const使用:定义常量、修饰函数参数、修饰函数返回值。
const 函数只能调用const函数,非const函数可以调用const函数。
- const变量:当一个变量被声明为
const
时,它的值就不能再被改变。这意味着一旦一个const
变量被初始化,它就不能被赋值或者以任何方式修改。如果在程序中尝试修改const
变量的值,编译器会给出错误提示。- 函数参数:在函数中,
const
可以用于修饰参数,表明该参数在函数内部不会被修改。这对于保证函数不会意外改变传入参数的值是很有帮助的。- 返回值:
const
也可以用于函数的返回类型,尤其是当函数返回一个对象的引用或者指针时。这样做可以保证返回的对象不会被调用者修改,从而保护了数据的完整性。不过,除非是重载操作符,否则不建议将返回值类型定为对某个对象的const
引用。- 成员函数:在类的成员函数后面加上
const
关键字,表示该成员函数不会修改类的任何成员变量。这样的函数通常被称为“const成员函数”,它们对于读取数据而不修改数据的操作非常有用。- 修饰局部变量:
const
可以用来修饰局部变量,确保这些变量在它们的作用域内不会被修改。例如,const int n = 5;
和int const n = 5;
都是表示变量n
的值不能被改变了。- 指针和引用:
const
还可以用来修饰指针和引用,分为指向const
的指针(指针指向的内容不可变)和const
指针(指针本身不可变,但指向的内容可变)。
函数参数使用常引用
可以提高程序的效率,因为函数不会为参数开辟新的内存空间,而是直接使用传入变量的内存地址。这样可以避免因参数复制而带来的额外内存开销。
const与define的区别
const和define在C++中都可以用来定义常量,但它们之间存在一些关键的区别。
- 类型检查与安全性:
const
定义的常量具有明确的数据类型,这意味着它会进行类型检查,有助于避免一些潜在的错误。而#define
仅仅是一个预处理指令,用于简单的字符替换,没有类型检查的过程。- 处理阶段:
const
常量在编译和运行时期被处理,而#define
是在预编译阶段处理的,这也影响了它们的使用方式和作用域。- 存储空间:
#define
宏不分配内存空间,因为它只是简单地被替换到源代码中。相比之下,const
常量会在内存中有实际的存储位置,可以存在于栈或堆中。- 调试和可变性:
const
常量可以进行调试,而且它允许在程序执行期间更改(只是在编译期间是不可以更改的)。相反,#define
宏在预编译后就被替换掉了,因此无法直接调试。- 条件编译:
#define
可以用于条件编译,例如防止头文件重复包含,而const
则不能用于此类条件编译场景。- 语言结构:
const
是C++语言的一部分,而#define
是一个预处理器的方法,所以使用const
通常比#define
快很多,因为它直接参与编译过程。- 适用范围:
const
可以用于类成员变量的定义,并且可以在类的内部使用。#define
由于是预处理器指令,不能定义在类内部。总的来说,
const
提供了一种更安全、更容易调试的方式来定义常量,并且它是C++语言本身支持的特性。而#define
是一种更原始且灵活的方式,它在预处理阶段就完成了所有的替换工作。在实际开发中,推荐使用const
来定义常量,除非有特定的理由需要使用
#define
。
typedef 和define的区别
C++中的typedef和define都是用于定义别名的,但它们之间有一些区别。
-
语法不同:
typedef是C++关键字,用于为类型定义别名。它的语法格式为:typedef 原类型名 别名;
。例如:typedef int MyInt;
define是预处理指令,用于定义宏。它的语法格式为:
#define 宏名 替换文本
。例如:#define PI 3.14159
-
作用范围不同:
typedef定义的别名具有作用域限制,它只在定义它的文件或代码块中有效。例如:// file1.cpp typedef int MyInt; // 仅在file1.cpp中有效
// file2.cpp // MyInt在这里是不可见的
define定义的宏没有作用域限制,它可以在整个程序中使用。例如:
// file1.cpp #define PI 3.14159 // 在整个程序中都有效
// file2.cpp double area = PI * r * r; // 可以使用PI宏
-
类型检查不同:
typedef可以为任何类型定义别名,包括基本类型、结构体、类等。例如:typedef struct { int x; int y; } Point;
define只能为常量表达式定义宏,不能为复杂类型定义宏。例如:
#define AREA(r) (3.14159 * (r) * (r)) // 可以定义一个计算圆面积的宏
举例说明:
使用typedef定义别名:
#include <iostream>
using namespace std;
typedef int MyInt; // 定义MyInt为int类型的别名
int main() {
MyInt a = 10; // 使用MyInt作为int类型的别名
cout << "a = " << a << endl;
return 0;
}
使用define定义宏:
#include <iostream>
using namespace std;
#define PI 3.14159 // 定义PI为3.14159的宏
int main() {
double r = 2.0;
double area = PI * r * r; // 使用PI宏计算圆面积
cout << "area = " << area << endl;
return 0;
}
sizeof 和strlen 的区别
- 作用对象:
sizeof
:用于获取一个变量或数据类型的字节大小。它可以用来计算整数、浮点数、数组、结构体等的大小。
strlen
:专门用于计算字符串的长度。它返回字符串中字符的数量,不包括字符串末尾的'\0'
结束符。- 参数类型:
sizeof
:不需要参数,它可以直接作用于变量、数据类型或表达式。
strlen
:需要一个字符串参数,通常是一个以'\0'
结尾的字符数组或指针。- 返回值:
sizeof
:返回的是字节数,通常以字节为单位。
strlen
:返回的是字符串的字符数。
例如,以下是使用 sizeof
和 strlen
的示例代码:
#include <stdio.h>
int main() {
int x = 42;
char str[] = "Hello, World!";
// 使用 sizeof
printf("Size of int: %lu\n", sizeof(x)); //4
printf("Size of char array: %lu\n", sizeof(str)); //14
// 使用 strlen
printf("Length of string: %d\n", strlen(str)); //13
// 空字符串的长度为 0
printf("Length of empty string: %d\n", strlen("")); //0
return 0;
}
在上面的示例中,sizeof(x)
返回变量 x
的字节大小,sizeof(str)
返回字符数组 str
的字节大小。而 strlen(str)
返回字符串 str
的字符长度。
需要注意的是,sizeof
是一个运算符,而 strlen
是一个函数。在使用时要注意它们的语法和适用场景。另外,strlen
只能用于计算字符串的长度,而 sizeof
可以用于计算各种数据类型的大小。
class 与struct 区别
默认访问权限:在C++中,struct的成员默认访问权限是public(公共的),而class的成员默认访问权限是private(私有的)。这意味着,在定义struct时,如果不指定访问修饰符,其成员将默认为公共的;而在定义class时,如果不指定访问修饰符,其成员将默认为私有的。
继承访问权限:当涉及到继承时,struct和class也有不同的默认行为。如果一个class派生自另一个class,那么默认的继承访问权限是private(私有的)。而如果一个struct派生自另一个struct或class,那么默认的继承访问权限是public(公共的)。
NULL与nullptr
C++中NULL定义为0,对于一个函数,如果它的重载分别是int和int*的话,那么此时传NULL就会出现歧义,因为原先的标准中,默认两个函数都可以使用NULL作为参数。
因此,C++为了避免这种歧义,引入了nullptr,此时nullptr只能作为指针传参,而不能作为整型传参。
数组与链表的区别
内存分配:数组在内存中是连续分配的,而链表的内存分配是不连续的。数组需要在声明时确定大小,这可能会导致空间的浪费或溢出的风险。相比之下,链表通过动态内存分配实现,只在需要时使用new分配内存空间,不需要时用delete释放,从而避免不必要的空间浪费。
插入和删除操作:在数组中插入或删除一个元素通常涉及到大量元素的移动,因为要维持数组的连续性。特别是在数组中间进行操作时,平均时间复杂度为O(n)。而在链表中,插入或删除操作仅涉及修改指针,因此更为高效;但是如果不知道操作位置,找到该位置的时间复杂度也是O(n)。所以链表在频繁进行插入和删除操作的场景下更具有优势。
性能考量:尽管访问数组和链表的元素理论上时间复杂度都是O(n),实际上数组的访问速度更快。这是因为CPU缓存会预读连续的内存空间,使得数组的元素可以被快速地加载到缓存中。与此相反,链表的分散存储方式不利于缓存的预读机制,导致实际访问速度较慢。
总的来说,数组更适合于固定大小且需频繁随机访问的情况,而链表适合于大小动态变化且需要频繁插入和删除操作的场景。选择使用数组还是链表,取决于具体的应用场景和性能要求。
static的作用
在 C++ 中,static
关键字有多种用途,常见的作用包括以下几种:
- 静态全局变量:使用
static
修饰全局变量,可以将其限制在源文件内部使用,而其他源文件无法访问该变量。这样可以避免全局变量在不同文件之间的命名冲突。
// 文件 1
static int globalVariable = 42;
// 文件 2
// 无法访问文件 1 中的 globalVariable
static全局变量 如果把文件1的头文件添加到文件2,那这个static全局变量也是可以在文件2直接使用的,如果不添加头文件,即使使用extern也会报错
2. 静态局部变量:在函数内部使用 static
修饰局部变量,该变量会在函数调用之间保持其值。即使函数返回后,下次再次调用该函数时,static
局部变量仍然保留之前的值。
void function() {
static int staticVariable = 0;
//...
}
- 静态成员变量:将类的成员变量声明为
static
,使得该变量属于类本身,而不属于类的任何特定对象。所有对象共享同一个static
成员变量,可以通过类名直接访问。
class MyClass {
public:
static int staticMember;
};
int MyClass::staticMember = 0;
// 可以通过 MyClass::staticMember 来访问和修改
- 静态成员函数:
static
成员函数也属于类本身,而不是类的对象。可以通过类名直接调用static
成员函数,而不需要创建类的对象。static
成员函数通常用于处理与类相关的全局操作,或者与类的对象无关的功能。
class MyClass {
public:
static void staticFunction() {
//...
}
};
MyClass::staticFunction();
// 直接调用静态成员函数
使用 static
关键字可以在不同的上下文提供一些特殊的特性和作用。具体的使用方式和效果取决于代码的需求和设计。需要根据具体情况合理使用 static
关键字,以实现所需的功能和数据封装。
同步与异步,阻塞与非阻塞,并行与并发
同步与异步:
- 同步:在同步操作中,一个任务的完成需要依赖另一个任务时,只有等待被依赖的任务完成后,依赖它的任务才能执行。例如,当你在银行柜台排队取钱时,你必须先等前一个人完成交易,然后才能进行你的交易。
- 异步:在异步操作中,不需要等待被依赖任务的完成,可以直接进行后续操作,当被依赖的任务完成时通过通知或回调来处理结果。例如,当你在网上购物时,你可以同时浏览多个商品并进行比较,而不需要等待每个页面加载完成后再进行下一个操作。
阻塞与非阻塞:
- 阻塞:在阻塞操作中,进程或线程在等待某些事件的完成(如I/O操作)时,无法继续执行其他操作。例如,当你在打电话时,你不能同时听音乐或进行其他活动。
- 非阻塞:在非阻塞操作中,进程或线程在不能立刻得到结果时,继续执行其他操作,不会被单一事件所阻。例如,当你在电脑上复制文件时,你可以同时打开其他程序或进行其他操作。
并行与并发:
- 并行:并行通常指多个任务在同一时刻同时执行。例如,多核处理器可以同时执行多个线程或进程。
- 并发:并发则是指多个任务在一段时间内交替运行,从宏观上看似乎是同时进行的,但在微观层面仍是轮流执行。例如,单核处理器可以通过时间片轮转技术实现多任务的并发执行。
同步和阻塞的区别在于,同步是指一个任务的完成需要依赖另一个任务时,只有等待被依赖的任务完成后,依赖它的任务才能执行。而阻塞是指进程或线程在等待某些事件的完成(如I/O操作)时,无法继续执行其他操作。
具体来说,同步和阻塞的区别可以从以下几个方面理解:
- 等待方式:同步是指在发出一个调用时,在没有得到结果之前,该调用就不返回。换句话说,是由调用者主动等待这个调用的结果。而阻塞是指在调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回。
- 通信机制:同步和异步关注的是消息通信机制。同步是指在发出一个调用时,在没有得到结果之前,该调用就不返回。而异步则是相反,调用在发出之后,这个调用就直接返回了,所以就没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果,而是在调用发出之后,被调用者通过“状态”、“通知”、“回调”三种途径通知调用者。
- 状态与过程:阻塞是线程的一种状态,同步是一种过程。线程如果阻塞了,但是如果被调用方(系统内核)通知返回数据,线程恢复运行状态,这就是异步的。反之,如果I/O采用同步方式,由调用方(应用层)主动问询系统内核获取数据,但是调用线程并不是阻塞状态,因为它在一直轮询系统内核数据是否准备好。
总结来说,同步和阻塞的主要区别在于等待方式、通信机制以及状态与过程。同步关注的是任务之间的依赖关系,而阻塞关注的是任务在等待过程中的状态。在实际应用中,合理运用同步和阻塞机制可以提高程序的性能和响应速度。
内联函数
定义:在函数定义体前加入关键字inline,使函数成为内联函数
原理:编译器收到内联函数的指示时,会在编译阶段使用函数的定义体来替代函数调用语句。
作用:节省了参数传递、控制转移等开销,提高函数的执行效率。
内联函数与宏的差别
1、内联函数要做类型检查,而宏不需要
2、宏是在代码处不加任何验证的简单替代,而内联函数是将代码直接插入到调用处,而减少了普通函数调用时的资源消耗
C++有了auto为什么还要decltype
auto
和 decltype
在C++中虽然都服务于类型推导,但它们的设计目的和使用场景是不同的,因此两者都是C++类型推导工具箱中的重要部分。
auto:
auto
关键字用于声明变量时自动推导变量的类型。当你初始化一个变量,但不确定或不想显式写出其类型时,auto
就非常有用。它使得代码更简洁,尤其是当类型很长或复杂时,如模板类型和lambda表达式的返回类型。
例子:
std::vector<int>::iterator it;
// 等价于
auto it = std::begin(vec);
// 或者
auto lambda = [](int x) { return x * x; };
decltype:
decltype
是一个关键字,用于推导表达式的类型。它的主要优势在于它能准确地推导出表达式的类型,包括任何const和引用限定符。decltype
通常用于模板元编程,或者当你需要知道一个复杂表达式的类型时。
例子:
int x = 10;
decltype(x + 1) y = x + 1; // y 的类型是 int
const int& ref = x;
decltype(ref) cref = ref; // cref 的类型是 const int&
为什么需要 decltype
尽管 auto
可以推导出变量的类型,但它不能用于推导表达式的类型,也不能用于函数参数或模板参数中。例如,你不能这样写:
template<typename T>
void foo(auto bar(T)) {} // 错误,auto 不能用于函数参数
在这种情况下,decltype
就派上了用场,它可以被用来准确地指定表达式的类型:
template<typename T>
void foo(typename decltype(bar(T)) param) {} // 正确
另外,decltype
可以与模板参数完美配合,允许你在模板中根据实际类型进行精确操作。例如,你可以使用 decltype
来推导一个模板参数的类型,然后基于这个类型进行进一步的模板元编程。
总之,auto
和 decltype
在C++中互补使用,auto
用于简化变量声明,而 decltype
则用于在需要表达式类型的地方提供类型信息,尤其是在模板和元编程中。
C++自带的强制转换比用()的转换有什么好处
更高的类型安全性,不同的强制类型转换关键字代表了不同的转换目的,使代码的意图更为明确,提高了代码的可读性和维护性。
QT MFC
QT线程
参考
以下是使用继承QThread和moveToThread的示例代码:
使用继承QThread的例子:
#include <QThread>
#include <QDebug>
class MyThread : public QThread {
public:
void run() override {
// 线程执行的任务逻辑
for (int i = 0; i < 10; ++i) {
qDebug() << "Thread running:" << i;
sleep(1); // 模拟耗时操作
}
}
};
int main() {
MyThread thread;
thread.start(); // 启动线程
thread.wait(); // 等待线程结束
return 0;
}
在这个例子中,我们创建了一个名为MyThread的类,它继承自QThread。我们在run()函数中定义了线程要执行的任务逻辑。然后,在main函数中创建了一个MyThread对象,并调用start()方法来启动线程。最后,我们调用wait()方法等待线程完成。
使用moveToThread的例子:
#include <QCoreApplication>
#include <QObject>
#include <QThread>
#include <QDebug>
class MyObject : public QObject {
Q_OBJECT
public slots:
void doWork() {
// 线程执行的任务逻辑
for (int i = 0; i < 10; ++i) {
qDebug() << "Thread running:" << i;
sleep(1); // 模拟耗时操作
}
}
};
int main(int argc, char *argv[]) {
QCoreApplication a(argc, argv);
MyObject obj;
QThread thread;
obj.moveToThread(&thread); // 将对象移动到新线程
QObject::connect(&thread, &QThread::started, &obj, &MyObject::doWork); // 连接信号槽
thread.start(); // 启动线程
thread.wait(); // 等待线程结束
return a.exec();
}
在这个例子中,我们创建了一个名为MyObject的类,它继承自QObject。我们在doWork()槽函数中定义了线程要执行的任务逻辑。然后,在main函数中创建了一个MyObject对象和一个QThread对象。我们将MyObject对象通过moveToThread()方法移动到新的线程中,并通过信号槽机制连接线程的started信号与对象的doWork槽函数。最后,我们调用start()方法启动线程,并调用wait()方法等待线程完成。
使用线程池QThreadPool的例子
线程池可以更有效地管理线程的生命周期,通过提交QRunnable任务到线程池中执行。
#include <QRunnable>
#include <QThreadPool>
class WorkerTask : public QRunnable {
public:
void run() override {
qDebug() << "Thread ID:" << QThread::currentThreadId();
}
};
int main(int argc, char *argv[]) {
QCoreApplication app(argc, argv);
QThreadPool *pool = QThreadPool::globalInstance();
pool->setMaxThreadCount(3); // 设置最大线程数
for (int i = 0; i < 10; ++i) {
pool->start(new WorkerTask());
}
app.exec();
return 0;
}
QT线程的优缺点
Qt的线程实现主要两种方式:继承于QThread和调用moveToThread函数。
1.继承于QThread
实现方法:
继承QThread
重写run函数
通过调用start函数来启动此线程。
缺点
①线程中的对象必须在run函数中创建。
②线程无法接收信号,只能发送信号。
③每次新建一个线程都需要继承QThread,实现一个新类,使用不太方便。
④要自己进行资源管理,线程释放和删除。并且频繁的创建和释放会带来比较大的内存开销。
⑤它只有run函数内部才是在线程范围内,其它函数(包括构造函数)都是在主线程中。
2.调用moveToThread函数
实现方式:
相对QThread线程方式来说,moveToThread使用更灵活,不需要继承QThread,也不用重写run函数。只需要将一个继承于QObject的类通过moveToThread移到QThread的一个对象中。
优缺点:
优点是克服了重写run的缺点,比较灵活简洁,但是不能在线程里面实现常驻任务(死循环的任务)。
细节注意点:
moveToThread 只有槽函数才会在新线程里执行
该类new的时候不要指定父对象:否则移动失败,提示 QObject::moveToThread: Cannot move objects with a parent。
总结
1、继承于QThread :生命周期长但是交互少的场景,常驻任务
2、moveToThread:常用于其他的一次性任务或者间歇性任务
QT的socket通信
服务端调用listen函数进行监听,是否有客户端与其进行连接;
客户端需要进行主动与客户端连接,调用connectToHost进行连接;
服务端:如果与客户端连接成功,服务器会触发newConnection信号;
客户端:如果与服务器连接成功,客户端会触发connected信号;
当断开连接,客户端的通信套接字会触发disconnected信号,服务器的通信套接字也会触发disconnected信号;
服务器:触发newConnection信号后,必须在槽函数中实例化QTcpSocket对象;
服务端与客户端都是通过自己的通信套接字使用wirte函数进行发送信息;
服务端与客户端接收到对方发过来的消息时,都会触发readyRead信号,然后就可以在对应槽函数做接受处理;
服务端与客户端的断开都是使用通信套接字调用disconnectFromHost函数进行断开处理。
QT数据库
#include <QtSql>
#include <QDebug>
// 创建或打开数据库连接
bool createConnection()
{
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("petfeeder.db");
if (!db.open()) {
qDebug() << "Failed to connect database.";
return false;
}
// 创建 petfeeder 表
QSqlQuery query;
bool ret = query.exec("CREATE TABLE IF NOT EXISTS petfeeder "
"(id INTEGER PRIMARY KEY AUTOINCREMENT, "
"interval INTEGER, temperature REAL, weight REAL)");
if (!ret) {
qDebug() << "Failed to create table: " << query.lastError().text();
}
return true;
}
// 插入数据
void insertData(int interval, double temperature, double weight)
{
QSqlQuery query;
QString sql = QString("INSERT INTO petfeeder (interval, temperature, weight) "
"VALUES (%1, %2, %3)").arg(interval).arg(temperature).arg(weight);
bool ret = query.exec(sql);
if (!ret) {
qDebug() << "Failed to insert data: " << query.lastError().text();
}
}
// 更新数据
void updateData(int id, int interval, double temperature, double weight)
{
QSqlQuery query;
QString sql = QString("UPDATE petfeeder SET interval=%1, temperature=%2, weight=%3 "
"WHERE id=%4").arg(interval).arg(temperature).arg(weight).arg(id);
bool ret = query.exec(sql);
if (!ret) {
qDebug() << "Failed to update data: " << query.lastError().text();
}
}
// 删除数据
void deleteData(int id)
{
QSqlQuery query;
QString sql = QString("DELETE FROM petfeeder WHERE id=%1").arg(id);
bool ret = query.exec(sql);
if (!ret) {
qDebug() << "Failed to delete data: " << query.lastError().text();
}
}
// 查询数据
void queryData()
{
QSqlQuery query("SELECT * FROM petfeeder");
while (query.next()) {
int id = query.value(0).toInt();
int interval = query.value(1).toInt();
double temperature = query.value(2).toDouble();
double weight = query.value(3).toDouble();
qDebug() << "Id:" << id << "Interval:" << interval << "Temperature:" << temperature << "Weight:" << weight;
}
}
// 主函数
int main()
{
if (!createConnection()) {
return 1;
}
// 插入数据
insertData(3, 25.5, 0.2);
insertData(2, 26, 0.3);
insertData(4, 24, 0.4);
// 查询数据
queryData();
// 更新数据
updateData(2, 4, 27, 0.3);
// 删除数据
deleteData(3);
// 查询数据
queryData();
return 0;
}
事务处理:
使用QSqlDatabase::transaction()方法开始一个事务。
在事务中执行多个SQL语句,如果所有操作都成功,则调用commit()方法提交事务;如果有错误发生,调用rollback()方法回滚事务。
事务处理确保了数据库操作的原子性,提高了数据的一致性和完整性。
数据库获取最后一次数据
last() :query指向结果集的最后一条记录。
QSS样式表
参考
Qt样式表设置函数:setStyleSheet
方式①
btn1->setStyleSheet("QPushButton{color:red}"); //设定前景颜色,就是字体颜色
btn1->setStyleSheet("QPushButton{background:yellow}"); //设定背景颜色为红色
方式②
QFile file(":/qss/main.qss");
file.open(QFile::ReadOnly);
QTextStream filetext(&file);
QString stylesheet = filetext.readAll();
this->setStyleSheet(stylesheet);
file.close();
QT容器类
顺序容器:
QList:这是一个动态数组,支持快速的头部和尾部插入操作,同时提供随机访问功能。它的内部实现并不是完全连续的,但如果需要连续内存空间,可以选择QVector。
QLinkedList:这是一个双向链表,插入和删除操作非常快速,但不支持直接索引访问。当需要在列表中间频繁进行插入和删除操作时,此容器是一个不错的选择。
QVector:类似于STL中的std::vector,提供连续内存存储,适用于大数据集并需要频繁随机访问的场合。由于数据存储在连续的内存块中,具有与静态数组相似的O(1)时间复杂度。
QStack:后进先出(LIFO)堆栈,是QVector的子类,只能通过push/pop操作来访问元素。
QQueue:先进先出(FIFO)队列,是QList的子类,遵循FIFO原则管理元素。
关联容器:
QMap:键值对映射容器,支持O(log n)的查找、插入和删除操作,键是唯一的。如果不需要键的顺序存储,QHash可能是一个更快的选择。
QHash:哈希表实现的键值对容器,查找效率极高,键也是唯一的。适合在不需要按键排序时使用。
QSet:集合容器,用于存储唯一值,不允许重复,提供快速的查找效率。
QT串口
Qt为串口通信提供了一套简单易用且功能丰富的编程框架,使得在Qt应用程序中实现串口功能变得直接且高效。以下是对Qt中串口通信的总结:
- 核心类
- QSerialPort:代表一个串口端口,提供了配置和操作串口的方法,如设置波特率、数据位等,以及打开、关闭、读取和写入数据等操作[1]。
- QSerialPortInfo:提供串口的信息,如端口名称、描述等,可用于枚举系统中可用的串口。
- 初始化和配置
- 使用
QSerialPortInfo
类来获取系统中可用的串口列表,通过availablePorts()
方法来实现。- 创建
QSerialPort
对象,并使用setPortName()
、setBaudRate()
、setDataBits()
等方法来配置串口。
- 读写操作
- 使用
open()
方法打开串口,使用close()
方法关闭串口。- 使用
write()
方法发送数据,使用read()
或readAll()
方法接收数据。- 通过信号槽机制,可以与
QSerialPort::readyRead
信号关联槽函数,以便在有数据可读时自动处理。
- 错误处理
QSerialPort
提供了error()
和errorString()
方法来获取串口操作的错误信息。- 应始终检查串口操作的返回状态,并合理处理错误情况。
举例:
假设我们有一个温度传感器连接到计算机的串口,我们想要读取传感器的温度数据,并在Qt应用程序中显示。
首先,我们需要包含必要的头文件:
#include <QSerialPort>
#include <QSerialPortInfo>
然后,我们可以创建一个QSerialPort
对象,并配置它:
QSerialPort serial;
serial.setPortName("COM1"); // 设置端口名称
serial.setBaudRate(QSerialPort::Baud9600); // 设置波特率
serial.setDataBits(QSerialPort::Data8); // 设置数据位
serial.setParity(QSerialPort::NoParity); // 设置校验位
serial.setStopBits(QSerialPort::OneStop); // 设置停止位
serial.setFlowControl(QSerialPort::NoFlowControl); // 设置流控制
if (serial.open(QIODevice::ReadWrite)) {
// 串口打开成功
} else {
// 串口打开失败,处理错误
}
接下来,我们可以使用read()
或readAll()
方法来读取数据:
if (serial.isOpen()) {
QByteArray data = serial.readAll(); // 读取所有可用数据
// 处理数据,例如解析温度值
}
最后,不要忘记在不再使用串口时关闭它:
serial.close();
通过上述示例,我们可以看到在Qt中实现串口通信是相对简单和直观的。Qt提供的串口类和方法使得开发者能够轻松地集成串口功能到他们的应用程序中,无论是简单的数据读取还是复杂的异步通信。
QT常见的事件机制
键盘事件 (QKeyEvent)
鼠标事件 (QMouseEvent)
拖放事件 (QDropEvent)
滚轮事件 (QWheelEvent)
绘屏事件 (QPaintEvent)
定时器事件 (QTimerEvent)
焦点事件 (QFocusEvent)
进入和离开事件 (QEnterEvent, QLeaveEvent)
移动事件 (QMoveEvent)
大小改变事件 (QResizeEvent)
显示和隐藏事件 (QShowEvent, QHideEvent)
窗口事件 (QWindowStateChangeEvent)
事件的处理:
每个QObject都有一个event()函数,它是事件处理的入口点。大多数时候,你不需要直接重写event(),而是重写更具体的事件处理函数,如paintEvent(), keyPressEvent(), mousePressEvent()等。
qt中有哪些类型的定时器?他们之间有什么区别?
在Qt中,主要存在三种类型的定时器,它们各自有着不同的特性和用途:
- QTimer
- QObject定时器事件(通过
startTimer()
) - QBasicTimer
下面详细介绍这三种定时器及其区别,并提供示例。
QTimer
QTimer
是Qt中最常用的定时器类,它继承自QObject
,并且提供了丰富的API来设置和控制定时器。QTimer
最显著的特点是它使用信号和槽机制,当定时器超时时,会发出timeout()
信号。
示例:
#include <QApplication>
#include <QLabel>
#include <QTimer>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QLabel label("Counting...");
label.show();
QTimer timer;
int count = 0;
timer.setInterval(1000); // 设置定时器间隔为1秒
connect(&timer, &QTimer::timeout, [&](){
count++;
label.setText(QString("Count: %1").arg(count));
});
timer.start();
return app.exec();
}
QObject定时器事件
通过QObject::startTimer()
方法,可以直接在QObject
的实例上创建一个定时器。当定时器超时时,Qt会向对象发送一个QTimerEvent
,你需要重写timerEvent()
函数来处理这个事件。
示例:
#include <QApplication>
#include <QLabel>
#include <QWidget>
class MyWidget : public QWidget {
Q_OBJECT
public:
MyWidget(QWidget *parent = nullptr) : QWidget(parent), count(0) {}
protected:
void timerEvent(QTimerEvent *event) override {
if (event->timerId() == timerId) {
count++;
label.setText(QString("Count: %1").arg(count));
} else {
QWidget::timerEvent(event);
}
}
private:
int count;
QLabel label;
int timerId;
};
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MyWidget widget;
widget.label.setText("Counting...");
widget.label.show();
widget.timerId = widget.startTimer(1000); // 创建定时器,间隔1秒
return app.exec();
}
QBasicTimer
QBasicTimer
是一个轻量级的定时器,主要用于性能敏感的场合。它不依赖于信号和槽,也不自动管理事件循环,因此使用起来更为低级。QBasicTimer
需要在每个事件循环中手动检查和更新。
示例:
#include <QApplication>
#include <QLabel>
#include <QWidget>
class MyWidget : public QWidget {
Q_OBJECT
public:
MyWidget(QWidget *parent = nullptr) : QWidget(parent), count(0) {}
protected:
void paintEvent(QPaintEvent *) override {
label.setText(QString("Count: %1").arg(count));
update();
}
void timerEvent() {
count++;
update();
}
void start() {
timer.start(1000, this); // 启动定时器,间隔1秒
}
private:
int count;
QLabel label;
QBasicTimer timer;
};
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MyWidget widget;
widget.label.setText("Counting...");
widget.label.show();
widget.start();
return app.exec();
}
总结
- QTimer是最常用且最容易使用的定时器,它使用信号和槽机制,提供了丰富的功能和配置选项。
- QObject定时器事件更接近底层,需要手动处理
timerEvent()
,适用于需要更精细控制的场景。 - QBasicTimer是最低级的定时器,性能最优,但需要手动管理和检查定时器状态,适用于高性能需求的应用。
QGraphicsView坐标体系基本概念
视图坐标系(View Coordinates):
视图坐标系是以QGraphicsView窗口的左上角为原点(0,0)的坐标系统。
在这个坐标系统中,X轴向右为正方向,Y轴向下为正方向。
场景坐标系(Scene Coordinates):
场景坐标系是由QGraphicsScene定义的,它有自己的原点和大小。
默认情况下,场景的原点(0,0)位于场景的左上角,但是可以通过setSceneRect()方法来设定场景的边界,从而改变原点的位置。
场景坐标系独立于视图的任何变换操作,即使视图进行了缩放或平移,场景的坐标仍然保持不变。
图元坐标系(Item Coordinates):
每个QGraphicsItem都有自己的本地坐标系,其中原点通常位于图元的中心或左上角,这取决于图元的类型和属性。
图元坐标系用于定义图元的形状、大小以及内部元素的位置。
变换矩阵(Transformation Matrices):
QGraphicsView使用一个变换矩阵来将场景坐标转换为视图坐标,这个矩阵包含了平移、旋转、缩放等操作。
使用setTransform()方法可以设置或修改变换矩阵,从而影响场景在视图中的呈现。
信号与槽的原理
信号与槽是Qt框架中一种用于对象间通信的机制,它基于观察者模式实现。
信号与槽的原理包括以下几个关键概念:
- 信号(Signal): 当某个对象的内部状态发生改变时,它会发出一个信号。这个信号是该对象的一个特殊成员函数,通常由关键字
signals
声明,并且使用emit
关键字来发出。- 槽(Slot): 槽是一个普通的成员函数,它被用来接收和响应信号。任何可以调用普通成员函数的地方都可以调用槽。槽可以是任何类的成员函数,但它们通常在与信号同一类中或者子类中定义。
- 连接(Connection): 信号与槽之间的连接是由程序员建立的。当信号被发出时,所有连接到该信号的槽函数都将被自动调用。一个信号可以连接到多个槽,反之亦然。
- 类型安全: 信号和槽机制是完全类型安全的。这意味着只有参数类型和数量都相匹配的信号和槽才能连接在一起。
- 松耦合: 信号和槽的连接实现了发送者和接收者之间的松散耦合。发出信号的对象不需要知道哪个对象的哪个槽函数会接收这个信号,这样使得组件更容易独立开发和重用。
- 多对多关系: 信号和槽之间是多对多的关系。这意味着一个信号可以关联多个槽,而一个槽也可以监听多个信号。
connect的第五个参数
Qt框架中的
connect
函数用于建立信号与槽的连接,其第五个参数是Qt::ConnectionType
,这个参数用来指定信号和槽连接的类型。
Qt::ConnectionType
是一个枚举类型,包含以下几种具体的连接方式:
Qt::AutoConnection
(默认值): 自动选择最适合的连接方式,通常是DirectConnection
或QueuedConnection
。Qt::DirectConnection
: 槽函数将在发出信号的对象的线程中直接被调用。如果信号发出者和槽函数所在线程不同,则可能导致槽函数执行时出现错误。Qt::QueuedConnection
: 槽函数将在接收者对象所在的线程中排队执行,这通常用于跨线程的信号和槽连接。Qt::BlockingQueuedConnection
: 类似于QueuedConnection
,但是调用者线程会等待槽函数执行完成才继续执行。Qt::UniqueConnection
: 标记该连接是唯一的,如果相同的信号和槽再次连接,将不会创建新的连接。总的来说,在使用这些连接类型时,需要根据应用程序的具体需求和上下文来选择合适的连接方式。例如,在多线程环境中,为了避免线程冲突,通常会使用
QueuedConnection
来实现跨线程的信号和槽通信。此外,使用BlockingQueuedConnection
时需要注意,由于它会阻塞调用者线程,可能会影响程序的性能,因此应谨慎使用。
举例QT的类
QPushButton:QPushButton类提供了一个可点击的按钮控件,可以用于触发某些操作或响应用户的输入。
QLabel:QLabel类提供了一个文本标签控件,可以显示单行或多行文本。
QLineEdit:QLineEdit类提供了一个单行文本编辑器控件,可以让用户输入和编辑文本。
QComboBox:QComboBox类提供了一个下拉列表框控件,可以让用户从多个选项中选择一个。
QSlider:QSlider类提供了一个滑动条控件,可以让用户在一个范围内选择一个值。
QTabWidget:QTabWidget类提供了一个选项卡控件,可以让用户在不同的页面之间切换。
QTextEdit:QTextEdit类提供了一个多行文本编辑器控件,可以让用户输入和编辑大量文本。
QTableWidget:QTableWidget类提供了一个表格控件,可以显示和编辑二维数据。
QTreeWidget:QTreeWidget类提供了一个树形控件,可以显示和编辑层次化的数据。。
QStackedWidget:QStackedWidget类提供了一个堆栈控件,可以在多个页面之间切换。
qss按钮切换颜色
/* style.qss */
QPushButton {
background-color: #3498db; /* 默认状态下的背景颜色 */
color: white; /* 默认状态下的文字颜色 */
}
QPushButton:hover {
background-color: #2980b9; /* 鼠标悬停状态下的背景颜色 */
}
QPushButton:pressed {
background-color: #27ae60; /* 按下状态下的背景颜色 */
}
QT自定义控件
QML
面试题
1.哪里出错
void foo()
{
std::vector<int> vec = {10, 6};
auto it = vec.begin(); //it原型是vector<int>::iterator
for(int i = 0; i < 100000; i++){
vec.push_back(i);
}
auto val = *it;
std::cout << val;
}
代码中的错误主要在于vec.push_back(i);这行操作之后,迭代器it已经不再指向原来的起始位置。在C++中,当向std::vector添加元素时,它会自动调整容量以容纳新元素。这意味着原来的元素可能会被移动到新的位置,而不仅仅是添加一个新元素。因此,当你尝试通过vec.begin ()获取的迭代器来访问元素时,你实际上是在访问一个可能已经被移动的位置。
具体来说,vec.push_back(i);操作后,vec的大小增加到了3(假设初始大小为2),并且原有的元素(10和6)被移动到了新的位置。因此,vec.begin ()返回的迭代器现在指向的是第一个原始元素(10),而不是第一个元素(10)。所以,当你执行auto val =*it;时,你实际上是在获取第一个原始元素(10),而不是你期望的第二个元素(6)。
正确的做法是,在调用push_back之后重新获取迭代器,以便它仍然指向正确的元素。例如,可以使用vec.end ()-1来获取最后一个元素之前的迭代器,这样无论push_back如何改变vec的大小,这个迭代器都能正确地指向最后一个元素。
综上所述,代码中的错误在于没有更新迭代器以反映向vec添加新元素后的变化。正确的实现应该在调用push_back后重新获取迭代器,或者使用其他方法来确保迭代器仍然指向正确的元素。
2.创建线程
第一步:创建两个线程,其中一个线程往屏幕上打印A,另外一个线程往屏幕上打印B(线程每执行一次只能打印一个B);
第二步:使用某种同步方法,在屏幕上循环打印:ABBABBABBABBABBABB
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
int nCount = 0; // 计数器,用于控制打印次数
void printA() {
for (int i = 0; i < 6; ++i) { // 打印3次A
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return nCount % 3 == 0; }); // 等待条件满足
std::cout << "A";
++nCount;
cv.notify_one(); // 唤醒另一个线程
}
}
void printB() {
for (int i = 0; i < 6; ++i) { // 打印6次B
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return nCount % 3 != 0; }); // 等待条件满足
std::cout << "B";
++nCount;
cv.notify_one(); // 唤醒另一个线程
}
}
int main() {
std::thread t1(printA);
std::thread t2(printB);
t1.join();
t2.join();
return 0;
}