top-level 不可以用于重载,用于重载无意义,因为换了值,还是表示一样的东西。不可以改变指针指向,但可以改变指针指向的内容,const就是修饰所指向的对象 ,实例:
const int a ; int* const a;
top level传参,相当于去掉中间的const(top level const can be droped)
low-level参数可以用于重载,因为他表示的是不同的东西。可以改变指针指向,但不能改变指向地址的内容,实例:
const int* a;
如何判断top/low level:对于复合类型(带*的),判断const距离变量的远近
对于简单类型,直接就是top level
#define NDEBUG
对assert语句不再care
3.C++重载,必须满足参数不同(个数上 || 类型上)
4.隐式调用 与explict
5.类内指针this实际上是
ClassName * const this,top-level类型, 定义的对象指针所指向的值不可变
6.类内static成员变量随着程序的 开始产生,随着程序的结束而销毁,跟具体对象无关,在对象外定义
可以通过className::static member来调用
7.stringstream ss,根据空格来分割
7.push_back和emplace_back的区别
https://zhuanlan.zhihu.com/p/213853588
C++ vector:: push_back 会先创建临时对象,然后将临时对象拷贝到容器中,最后销毁临时对象;但是 emplace_back 仅会在容器中原地创建一个对象出来,减少临时对象拷贝、销毁的步骤,所以性能更高
协程问题
协程就是用户态线程,协程的调度完全由开发者进行控制,因此实现协程的关键也就是 实现一个用户态线程的调度器,由于协程是在用户态中实现调度,避免了内核态的上下文切换造成的性能损失,从而突破了线程在IO上的性能瓶颈。
宏定义define
#define LT_INTERFACE_IMPLEMENTATION(return_type ,default_return, object_name ,func_name, formal_args, real_args) return_type lt_##func_name formal_args\
{\
LT_INTERFACE_CHECK(object_name,default_return)\
LT_INTERFACE_CALL(func_name,real_args)\
}
这是一个宏定义,定义了一个名为 LT_INTERFACE_IMPLEMENTATION
的宏。
宏的定义如下:
#define LT_INTERFACE_IMPLEMENTATION(return_type ,default_return, object_name ,func_name, formal_args, real_args) return_type lt_##func_name formal_args\
{\
LT_INTERFACE_CHECK(object_name,default_return)\
LT_INTERFACE_CALL(func_name,real_args)\
}
#define LT_INTERFACE_CHECK(object_name, default_return) \
if (lt.obj_type != CT_RUNTIME && lt.obj_type != CT_EVALUATE)\
{\
return default_return; \
}\
object_name* c = (object_name*)(lt.obj_ptr); \
if (c == nullptr)\
{\
return default_return; \
}
#define LT_INTERFACE_CALL(func_name, args) return c->func_name args;
这个宏的目的是简化实现对象方法的代码。它接受六个参数,分别是 return_type
、default_return
、object_name
、func_name
、formal_args
、real_args
。
解释一下这些参数的含义:
return_type
:表示函数的返回类型。default_return
:默认情况下函数的返回值。object_name
:表示对象的名称。func_name
:表示函数的名称。formal_args
:表示函数的形式参数列表。real_args
:表示函数的实际参数列表。
宏的展开部分如下:
return_type lt_##func_name formal_args
{
LT_INTERFACE_CHECK(object_name, default_return)
LT_INTERFACE_CALL(func_name, real_args)
}
在展开后的代码中,lt_##func_name
是一个新的函数名,它是由 lt_
前缀和 func_name
组成的。这样可以确保每个函数都有一个唯一的名称,避免命名重复。
宏展开后的代码包括两个主要部分:
LT_INTERFACE_CHECK(object_name, default_return)
:这是一个宏调用,它用于检查对象是否合法。如果对象不合法,会返回default_return
。LT_INTERFACE_CALL(func_name, real_args)
:这也是一个宏调用,用于调用实际的函数代码。
通过使用这个宏定义,可以简化对象方法的实现代码,并且可以在每个方法中添加一些通用的检查逻辑。
#define LOG_INFO(…)
#define LOG_INFO(…) logline(LLV_INFO,FILE,func,LINE).print(VA_ARGS);
#define LOG_INFO(…) logline(LLV_INFO,FILE,func,LINE).print(VA_ARGS); 是一个预处理指令,用于定义一个宏 LOG_INFO。
在这个宏定义中,… 代表一个可变参数的占位符,它允许宏接受任意数量的参数。
这个宏将被展开成一个函数调用形式,其中 logline(LLV_INFO,FILE,func,LINE) 是一个返回日志记录器对象的表达式,.print(VA_ARGS) 是对该日志记录器对象的 print 函数的调用。
这个宏可以用于打印信息级别为 LLV_INFO 的日志信息,并在日志中包含文件名、函数名和所在行号。FILE、func 和 LINE 是预定义的宏,它们分别表示当前源文件的文件名、当前函数的名称以及代码行的行号。
例如,使用 LOG_INFO(“Hello, world!”) 将展开成类似于 logline(LLV_INFO, FILE, func, LINE).print(“Hello, world!”) 的代码。
参数模板
template <typename Frist, typename... Types>
typename std::enable_if < !std::is_enum <Frist>::value, void >::type
print(Frist firstArg, Types... args) {
_sd <<static_cast<std::decay_t<Frist>>(firstArg)<<" ";
print(args...);
}
这是一个递归的可变参数模板函数 print
的定义。
typename std::enable_if < !std::is_enum <Frist>::value, void >::type
是一个模板参数,用于根据条件启用或禁用该函数的特化版本。在这种情况下,它禁用了函数print
的特化版本,除非类型First
不是枚举类型。void
是函数的返回类型,表示函数没有返回值。
函数 print
接受一个参数 firstArg
,表示作为第一个参数打印输出的值,以及可变数量的额外参数 args
。
在函数体内部,首先将第一个参数 firstArg
转换为 std::decay_t<First>
,并将其输出到 _sd
流中。std::decay_t
是一个类型转换工具,用于移除类型的引用和修饰符。
然后,该函数调用自身递归地传递剩余的参数 args
,这样就可以按顺序将所有参数打印到 _sd
流中。
这个函数的递归调用会在没有参数时终止,结束递归的条件是参数列表为空。
这个函数模板 print
可以用于递归地打印一个或多个参数的值,将它们依次输出到 _sd
流中,除非第一个参数的类型是枚举类型,否则函数将禁用特化版本。
C++ 模板初始化列表展开
template<typename T, typename... Ts>
auto printf3(T value, Ts... args) {
std::cout << value << std::endl;
(void) std::initializer_list<T>{([&args] {
std::cout << args << std::endl;
}(), value)...};
}
通过初始化列表,(lambda 表达式, value)… 将会被展开。由于逗号表达式的出现,首先会执行
前面的 lambda 表达式,完成参数的输出。为了避免编译器警告,我们可以将 std::initializer_list
显式的转为 void。
这是一个使用可变参数模板和初始化列表的函数模板 printf3
的示例。该函数模板的作用是打印参数列表中的所有值,并在每个值打印之前使用初始化列表先执行一段代码。
让我们逐行解释这段代码的工作原理:
-
template<typename T, typename... Ts>
:这是一个使用可变参数模板的函数模板定义,它接受任意数量的参数,其中第一个参数类型为T
,剩余参数类型为Ts
(采用模板参数包的方式表示)。 -
auto printf3(T value, Ts... args)
:函数模板printf3
的声明,接受一个参数value
,以及可变参数args
。 -
std::cout << value << std::endl;
:打印参数value
的值。 -
(void) std::initializer_list<T>{([&args] {std::cout << args << std::endl; }(), value)...};
:使用初始化列表和lambda表达式,先执行一段代码,再对每个参数值进行初始化。a.
(void) std::initializer_list<T>{...};
:创建一个初始化列表,其中元素的类型为T
。这会触发初始化列表的初始化过程。b.
([&args] {std::cout << args << std::endl; }(), value)...
:使用展开表达式为每个参数args
创建一个 lambda 表达式,该表达式打印参数值。c.
()
:在每个 lambda 表达式内部使用额外的()
运算符,以使其在初始化列表初始化之前执行。d.
value
:使用参数value
来初始化每个初始化列表中的元素。
综上所述,函数模板 printf3
的作用是先打印参数 value
的值,然后在打印每个可变参数值之前,先执行一段代码。通过使用展开表达式和初始化列表,可以循环处理可变参数列表中的每个参数值,并在执行代码之前进行打印。
(void)的作用
在上述代码中,(void)
的作用是将表达式强制转换为 void
类型。这是为了避免在初始化列表中使用 lambda 表达式时,生成多余的临时对象。
在 C++ 中,lambda 表达式是有类型的,并且会生成一个匿名的闭包对象。然而,在某些上下文中,我们可能只关注 lambda 表达式内部代码的执行效果,并不需要使用其返回的闭包对象。为了避免在初始化列表中生成无用的闭包对象,我们可以使用 (void)
将表达式转换为 void
类型,这样就不会生成多余的临时对象。
在上述代码中,将 (void)
放在 lambda 表达式前面,相当于将 lambda 表达式的结果强制转换为 void
类型。这样,我们可以在初始化列表中执行代码段,而不需要关心 lambda 表达式生成的闭包对象。
需要注意的是,这种技巧仅在我们关注代码段的执行效果,并且不需要使用闭包对象时才可用。 如果我们需要使用闭包对象,则不应将其强制转换为 void
类型。
C++17之后折叠模板参数的展开
template<typename ... T>
auto sum(T ... t) {
return (t + ...);
}
这是一个使用折叠表达式的示例函数模板 sum
,它接受任意数量的参数并返回它们的总和。
让我们逐行解释这段代码的工作原理:
-
template<typename ... T>
:这是一个可变参数模板的函数模板定义,它接受任意数量的参数,参数类型由模板参数包T
表示。 -
auto sum(T ... t)
:函数模板sum
的声明,接受一个参数包t
,其中的参数类型匹配模板参数包T
。 -
(t + ...)
:折叠表达式(t + ...)
用于计算参数列表中所有参数的总和。t
:代表参数包t
中的一个参数。+
:用于将参数进行逐个相加的操作符。...
:折叠表达式的语法,表示将所有参数进行展开。
当该表达式被展开时,它会形成一系列的加法运算,将参数列表中所有的值相加。
-
返回结果
(t + ...)
:该表达式的结果即为参数列表中所有参数的总和。
综上所述,函数模板 sum
使用折叠表达式将参数列表中所有参数进行相加,然后返回它们的总和。该函数模板可以接受任意数量的参数,并且参数的类型可以是任意的,只要能够进行加法操作即可。
非类型模板参数
template <typename T, int BufSize>
class buffer_t {
public:
T& alloc();
void free(T& item);
private:
T data[BufSize];
}
buffer_t<int, 100> buf; // 100 作为模板参数
在这种模板参数形式下,我们可以将 100 作为模板的参数进行传递。
这是一个模板类 buffer_t
的定义,它使用类型参数 T
和非类型参数 BufSize
,表示缓冲区的大小和存储的类型。
让我们逐行解释这段代码的工作原理:
-
template <typename T, int BufSize>
:这是一个使用类型参数和非类型参数的类模板定义,其中类型参数T
表示缓冲区存储的类型,非类型参数BufSize
表示缓冲区的大小。 -
class buffer_t { ... }
:类模板buffer_t
的声明。 -
T& alloc();
:alloc
是一个公共成员函数,返回一个T
类型的引用。它用于从缓冲区中分配一个元素。 -
void free(T& item);
:free
是一个公共成员函数,接受一个T
类型的引用参数item
。它用于释放缓冲区中的一个元素。 -
T data[BufSize];
:私有成员变量data
是一个大小为BufSize
的数组,用于存储缓冲区中的元素。
综上所述,buffer_t
是一个模板类,它可以通过指定类型参数和非类型参数来创建具有指定大小和存储类型的缓冲区。alloc
函数用于分配缓冲区中的一个元素,而 free
函数用于释放缓冲区中的一个元素。实际存储的元素在 data
数组中。
委托构造
C++11 引入了委托构造的概念,这使得构造函数可以在同一个类中一个构造函数调用另一个构造函
数,从而达到简化代码的目的
class Base {
public:
int value1;
int value2;
Base() {
value1 = 1;
}
Base(int value) : Base() { // 委托 Base() 构造函数
value2 = value;
}
}
继承构造
在传统 C++ 中,构造函数如果需要继承是需要将参数一一传递的,这将导致效率低下。C++11 利
用关键字 using 引入了继承构造函数的概念
class Base {
public:
int value1;
int value2;
Base() {
value1 = 1;
}
Base(int value) : Base() { // 委托 Base() 构造函数
value2 = value;
}
};
class Subclass : public Base {
public:
using Base::Base; // 继承构造
};
int main() {
Subclass s(3);
std::cout << s.value1 << std::endl;
std::cout << s.value2 << std::endl;
}
C++ 11枚举类
enum class new_enum : unsigned int {
value1,
value2,
value3 = 100,
value4 = 100
};
在这个语法中,枚举类型后面使用了冒号及类型关键字来指定枚举中枚举值的类型,这使得我们能
够为枚举赋值(未指定时将默认使用 int)。
枚举类的输出
std::enable_if
std::is_enum::value
std::underlying_type::type
static_cast
template<typename T>
std::ostream& operator<<(
typename std::enable_if<std::is_enum<T>::value,
std::ostream>::type& stream, const T& e)
{
return stream << static_cast<typename std::underlying_type<T>::type>(e);
}
这是一个用于枚举类型的输出流重载运算符 operator<<
的模板定义。
-
template<typename T>
:这是一个使用类型参数T
的模板定义,表示要重载的输出流运算符的参数类型。 -
std::ostream& operator<<(typename std::enable_if<std::is_enum<T>::value, std::ostream>::type& stream, const T& e)
:这是输出流重载运算符operator<<
的声明。它接受两个参数:stream
:一个引用,表示输出流对象。e
:一个常量引用,表示要输出的枚举类型对象。
-
typename std::enable_if<std::is_enum<T>::value, std::ostream>::type&
:在模板参数列表中使用std::enable_if
,控制输出流重载运算符只对枚举类型生效。当且仅当T
是枚举类型时,才选择这个重载运算符。返回类型为std::ostream&
。 -
return stream << static_cast<typename std::underlying_type<T>::type>(e);
:运算符重载的具体实现。首先使用std::underlying_type
获取枚举类型T
的底层类型,然后使用static_cast
将枚举值e
转换为底层类型并通过输出流stream
进行输出。
综上所述,该模板函数定义了一个用于枚举类型的输出流重载运算符 operator<<
。当对枚举类型进行输出时,它会将枚举值转换为底层类型,并通过输出流进行输出。这样,在输出枚举类型对象时,可以直接使用 std::cout << obj
来输出其底层数值表示。
lamda
值捕获
void lambda_value_capture() {
int value = 1;
auto copy_value = [value] {
return value;
};
value = 100;
auto stored_value = copy_value();
std::cout << "stored_value = " << stored_value << std::endl;
// 这时, stored_value == 1, 而 value == 100.
// 因为 copy_value 在创建时就保存了一份 value 的拷贝
}
引用捕获
void lambda_reference_capture() {
int value = 1;
auto copy_value = [&value] {
return value;
};
value = 100;
auto stored_value = copy_value();
std::cout << "stored_value = " << stored_value << std::endl;
}
这时, stored_value == 100, value == 100.
因为 copy_value 保存的是引用
表达式捕获
void lambda_expression_capture() {
auto important = std::make_unique<int>(1);
auto add = [v1 = 1, v2 = std::move(important)](int x, int y) -> int {
return x+y+v1+(*v2);
};
std::cout << add(3,4) << std::endl;
}
important 是一个独占指针,是不能够被 “=” 值捕获到,这时候我们可以将其转
移为右值,在表达式中初始化。
C++14 泛型lamda
auto add = [](auto x, auto y) {
return x+y;
};
add(1, 2);
std::function
当 Lambda 表达式的捕获列表为空时,闭包对象还能够转换为函数指针值进行传递
using foo = void(int); // 定义函数类型, using 的使用见上一节中的别名语法
void functional(foo f) { // 参数列表中定义的函数类型 foo 被视为退化后的函数指针类型 foo*
f(1); // 通过函数指针调用函数
}
int main() {
auto f = [](int value) {
std::cout << value << std::endl;
};
functional(f); // 传递闭包对象,隐式转换为 foo* 类型的函数指针值
f(1); // lambda 表达式调用
return 0;
}
一种是将 Lambda 作为函数类型传递进行调用,
而另一种则是直接调用 Lambda 表达式
std::bind
int foo(int a, int b, int c) {
;
}
int main() {
auto bindFoo = std::bind(foo, std::placeholders::_1, 1,2);
将参数 1,2 绑定到函数 foo 上,
但使用 std::placeholders::_1 来对第一个参数进行占位
这时调用 bindFoo 时,只需要提供第一个参数即可
右值引用
- const char*& pl = “01234”;
这行代码是非法的,因为你不能将字符串字面量绑定到非常量左值引用。
在这里,"01234"
是一个字符串字面量,它是一个 const char[6]
类型的常量字符数组。左值引用 const char*&
表示引用一个指向 const char
类型的非常量指针的引用。
尝试将字符串字面量赋值给一个非常量左值引用会导致编译错误,因为字符串字面量是一个常量,不能通过非常量引用进行修改。正确的方式应该是将字符串字面量赋值给 const char*
类型的常量指针或右值引用。
- const char*&& pr = “01234”;
这是一个将字符串字面量绑定到右值引用的示例代码。
让我们逐个元素解释这段代码的含义:
-
const char*&& pr
:这是一个声明语句,定义了一个右值引用变量pr
,类型为const char*&&
。右值引用表示其绑定的值为一个临时对象或将要销毁的对象。 -
= "01234"
:使用赋值运算符将字符串字面量"01234"
赋值给右值引用变量pr
。需要注意的是,字符串字面量是一个常量字符数组,可以自动转换为const char*
类型。
综上所述,这段代码将字符串字面量 "01234"
与右值引用变量 pr
绑定。右值引用变量可以用于引用将要销毁的临时对象,因此它可以用于延长字符串字面量的生命周期,直到右值引用变量 pr
超出范围。
- const char* p = “01234”;
正确,“01234” 被隐式转换为 const char*,字符串字面量赋值给 const char*
类型的常量指针
移动语义
#include <iostream> // std::cout
#include <utility> // std::move
#include <vector> // std::vector
#include <string> // std::string
int main() {
std::string str = "Hello world.";
std::vector<std::string> v;
// 将使用 push_back(const T&), 即产生拷贝行为
v.push_back(str);
// 将输出 "str: Hello world."
std::cout << "str: " << str << std::endl;
// 将使用 push_back(const T&&), 不会出现拷贝行为
// 而整个字符串会被移动到 vector 中,所以有时候 std::move 会用来减少拷贝出现的开销
// 这步操作后, str 中的值会变为空
v.push_back(std::move(str));
// 将输出 "str: "
std::cout << "str: " << str << std::endl;
return 0;
}
使用 push_back(const T&), 产生拷贝行为,不会改变源字符串
使用 push_back(const T&&), 不会出现拷贝行为,会将源字符串搬空
class A {
public:
int *pointer;
A():pointer(new int(1)) {
std::cout << " 构造" << pointer << std::endl;
}
A(A& a):pointer(new int(*a.pointer)) {
std::cout << " 拷贝" << pointer << std::endl;
} // 无意义的对象拷贝
A(A&& a):pointer(a.pointer) {
a.pointer = nullptr;
std::cout << " 移动" << pointer << std::endl;
}
~A(){
std::cout << " 析构" << pointer << std::endl;
delete pointer;
}
};
// 防止编译器优化
A return_rvalue(bool test) {
A a,b;
if(test) return a; // 等价于 static_cast<A&&>(a);
else return b; // 等价于 static_cast<A&&>(b);
}
int main() {
A obj = return_rvalue(false);
std::cout << "obj:" << std::endl;
std::cout << obj.pointer << std::endl;
std::cout << *obj.pointer << std::endl;
return 0;
}
这段代码展示了一个类 A
的定义和一个使用右值引用的函数 return_rvalue
的示例。
-
class A { ... }
:定义了一个类A
。 -
int *pointer;
:A
类中的公共成员变量pointer
是一个指向int
类型的指针。 -
A()
:默认构造函数会为pointer
分配内存,并将其指向一个新创建的int
对象。 -
A(A& a)
:拷贝构造函数会创建一个新的int
对象,并将a.pointer
指向的值复制到新对象中。 -
A(A&& a)
:移动构造函数接受一个右值引用参数a
。它会接管a.pointer
的所有权,并将其指向的内存地址赋值给当前对象的pointer
。然后,将a.pointer
设置为nullptr
,防止在a
析构时重复释放内存。 -
~A()
:析构函数会释放pointer
指向的内存,并删除int
对象。 -
A return_rvalue(bool test)
:这是一个返回A
类型对象的函数。根据传入的test
参数,它要么返回对象a
,要么返回对象b
。返回过程中会使用右值引用进行移动构造。 -
int main()
:定义了主函数。 -
A obj = return_rvalue(false);
:创建了一个A
类型的对象obj
,并通过调用return_rvalue
函数进行初始化。此处使用右值引用进行移动构造。 -
std::cout << "obj:" << std::endl;
:输出字符串 “obj:”。 -
std::cout << obj.pointer << std::endl;
:输出obj.pointer
的值,即pointer
所指向的地址。 -
std::cout << *obj.pointer << std::endl;
:输出obj.pointer
所指向的值。
综上所述,这段代码展示了使用右值引用进行对象移动构造的示例。通过在构造函数和移动构造函数中输出指针的地址,可以观察对象的构造、拷贝或移动以及析构的过程。同时,return_rvalue
函数通过条件语句返回同一类型的不同对象,演示了右值引用的使用方法。
输出的结果
-
return_rvalue(false)
会返回对象b
。 -
对象
obj
通过使用右值引用进行移动构造,其pointer
成员将指向与对象b
中的pointer
成员相同的地址。 -
在主函数中,首先输出字符串 “obj:”。
-
接下来,输出
obj.pointer
的值,即pointer
所指向的地址。这将是一个有效的内存地址,但它是不确定的。 -
最后,输出
*obj.pointer
的值。由于pointer
指向的是b
中的pointer
,而b
在其生命周期结束时会删除pointer
,所以输出的值也将是不确定的。
因此,这段代码的输出结果可能会是:
obj:
<some memory address>
<undefined value>
请注意,具体的输出结果可能与这些推测不完全一致,因为输出受多个因素的影响,包括编译器、处理器架构和操作系统等。
std::array和std::vector
与 std::vector 不同,std::array 对象的大小是固定的,如果容器大小是固
定的,那么可以优先考虑使用 std::array 容器。另外由于 std::vector 是自动扩容的(*2操作),当存入大量的
数据后,并且对容器进行了删除操作,容器并不会自动归还被删除元素相应的内存,这时候就需要手动
运行 shrink_to_fit() 释放这部分内存。
不同于 C 风格数组,std::array 不会自动退化成 T*
std::array<int, len> arr = {1, 2, 3, 4};
int *arr_p = arr;
正则表达式
std::regex base_regex("([a-z]+)\\.txt");
std::smatch base_match;
for(const auto &fname: fnames) {
if (std::regex_match(fname, base_match, base_regex)) {
// std::smatch 的第一个元素匹配整个字符串
// std::smatch 的第二个元素匹配了第一个括号表达式
if (base_match.size() == 2) {
std::string base = base_match[1].str();
std::cout << "sub-match[0]: " << base_match[0].str() << std::endl;
std::cout << fname << " sub-match[1]: " << base << std::endl;
}
}
}
std::smatch 的第一个元素匹配整个字符串
std::smatch 的第二个元素匹配了第一个括号表达式
依次传入 std::string/std::smatch/std::regex 三个参数,其中
std::smatch 的本质其实是 std::match_results。故而在标准库的实现中,std::smatch 被定
义 为 了 std::match_resultsstd::string::const_iterator,也 就 是 一 个 子 串 迭 代 器 类 型 的
match_results。使用 std::smatch 可以方便的对匹配的结果进行获取
或者
int main() {
std::string fnames[] = {"foo.txt", "bar.txt", "test", "a0.txt", "AAA.txt"};
// 在 C++ 中 \ 会被作为字符串内的转义符,
// 为使 \. 作为正则表达式传递进去生效,需要对 \ 进行二次转义,从而有 \\.
std::regex txt_regex("[a-z]+\\.txt");
for (const auto &fname: fnames)
std::cout << fname << ": " << std::regex_match(fname, txt_regex) << std::endl;
}
C++函数指针多态的原理
C++ 的函数指针多态是通过函数指针的动态绑定和虚函数机制来实现的。
在 C++ 中,函数指针是指向函数的指针变量,它可以存储并调用对应类型的函数。多态是面向对象编程中的概念,允许不同类型的对象通过相同的调用方式进行操作,实现了程序的灵活性和可扩展性。
下面是函数指针多态的实现原理:
- 定义一个基类,其中包含一个虚函数(成员函数)作为接口。
- 派生一个或多个子类,重写基类的虚函数,并提供具体的实现。
- 通过基类指针或引用调用虚函数,实现运行时的动态绑定。
- 创建一个函数指针,该指针的类型与基类的虚函数类型匹配。
- 将函数指针指向不同的派生类的虚函数。
- 通过调用函数指针执行相应的虚函数,实现多态行为。
函数指针多态的核心概念是通过函数指针的动态绑定,让程序在运行时决定调用哪个具体的函数实现。这样,在不改变函数指针的情况下,可以动态地切换函数的行为,从而实现多态。
下面是一个简单的示例代码,演示了函数指针多态的原理:
#include <iostream>
// 基类
class Base {
public:
virtual void display() {
std::cout << "Base Class" << std::endl;
}
};
// 派生类
class Derived : public Base {
public:
void display() override {
std::cout << "Derived Class" << std::endl;
}
};
int main() {
// 创建基类和派生类对象
Base baseObj;
Derived derivedObj;
// 定义函数指针,指向基类的虚函数
void (Base::*ptr)() = &Base::display;
// 使用基类指针,调用基类的虚函数
(baseObj.*ptr)(); // 输出:Base Class
// 使用基类指针,调用派生类的虚函数
(derivedObj.*ptr)(); // 输出:Derived Class
// 将函数指针指向派生类的虚函数
ptr = &Derived::display;
// 使用基类指针,通过函数指针调用派生类的虚函数
(baseObj.*ptr)(); // 输出:Derived Class
return 0;
}
在上述代码中,定义了一个基类 Base
和一个派生类 Derived
。基类中有一个虚函数 display
,派生类重写了该虚函数。
通过定义函数指针 ptr
,可以分别指向基类和派生类的虚函数。通过调用函数指针,可以实现多态行为。
typedef和Define的区别
typedef
和 #define
都可用于定义类型别名或宏,但它们之间有一些关键的区别:
-
作用范围不同:
typedef
的作用范围是局部的,只在定义它的作用域内有效。例如,在函数内部定义的typedef
只在该函数内部可见。#define
的作用范围是全局的,它是对代码中所有位置都有效的。
-
类型别名 vs. 宏定义:
typedef
用于创建类型别名,它将一个已有的类型名称定义为新的别名。这使得代码更具可读性,并提供了更好的类型检查。#define
用于创建宏定义,它将一个标识符定义为一个文本替换。
-
编译时处理的时机不同:
typedef
是在编译时处理的,它在编译器的语法分析阶段进行处理。#define
是在预处理阶段进行处理的,它是在编译器正式编译代码之前,在文本替换阶段进行处理。
-
适用范围不同:
typedef
主要用于定义新的类型名称,如给复杂类型起一个更易读的别名。例如,typedef int MyInt;
将int
类型定义为MyInt
。#define
不仅可以定义常量,还可以定义宏函数、字符串等复杂的替换规则。
综上所述,typedef
更适合用于定义类型别名,而 #define
更适合用于宏定义和替换文本。选择使用哪种方式取决于具体的需求和语境。
C++函数在什么场景下分别使用指针和引用传递参数
你什么情况用指针当参数,什么时候用引用,为什么?
使用引用参数的主要原因有两个:
程序员能修改调用函数中的数据对象
通过传递引用而不是整个数据–对象,可以提高程序的运行速度
一般的原则: 对于使用引用的值而不做修改的函数:
-
如果数据对象很小,如内置数据类型或者小型结构,则按照值传递;
-
如果数据对象是数组,则一般使用指针,并且指针声明为指向const的指针;
-
如果数据对象是较大的结构,则使用const指针或者引用,已提高程序的效率。这样可以节省结构所需的时间和空间;
-
如果数据对象是类对象,则使用const引用(传递类对象参数的标准方式是按照引用传递);
对于修改函数中数据的函数:
- 如果数据是内置数据类型,则使用指针
- 如果数据对象是结构,则使用引用或者指针
- 如果数据是类对象,则使用引用
有一种说法认为:“如果数据对象是数组,则只能使用指针”,这是不对的,比如
template<typename T, int N>
void func(T (&a)[N])
{
a[0] = 2;
}
int main()
{
int a[] = { 1, 2, 3 };
func(a);
cout << a[0] << endl;
return 0;
}
其中T (&a)[N]参数的含义:
T (&a)[N]
是一个引用数组参数的声明方式。
T
是数组中元素的类型。&a
表示这是一个引用参数,它引用了一个数组。[N]
指定数组的大小,这里的N
是一个编译时常量。
整个声明的意思是,参数 a
是一个引用,它引用了一个具有类型 T
的数组,并且该数组的大小为 N
。这样,在函数内部通过操作参数 a
,可以直接操作原始数组。
在上面的示例中,我们使用了这种参数声明方式,可以在函数 func
内部修改传递进来的数组 a
的值。这是通过引用数组参数实现数组修改的一种常见方式。