文章目录
前言
C++ 的细节语法,项目中遇到的问题在这里统一总结,持续更新
一、C++函数默认参数
class A {
void func(int a = 20);
};
void A::func(int a)
{
std::cout << a << std::endl;
}
成员函数的缺省参数在声明时定义即可,包括模板的默认参数也是在声明时定义即可。
二、函数返回值是函数指针
#include <iostream>
using namespace std;
void f()
{
cout << 10 << endl;
}
void (*s(void (*f)()))()
{
return f;
}
int main()
{
void (*t)();
t = s(f);
t();
return 0;
}
这里的 s 是一个函数,返回值类型是函数指针,参数类型和个数在最后的()中书写。
void (*t[n])();
函数指针数组的写法
三、通过new创建vector的元素访问方式
四、类中成员变量包含vector,可以在初始化列表中初始化
class A
{
std::vector<int> s;
A(int size, int a) : s(size, a) {}
};
五、for循环中的变量地址始终不变
for 循环中的局部变量地址始终是固定的,此时如果局部变量为指针,可能会引起某些错误的结果,例如我们需要存储指针的中间变量,这样在for循环结束时,存储的值会是最后一轮循环局部变量指向的对象的值。
#include <iostream>
#include <vector>
int main()
{
std::vector<int*> s;
for(int i = 0; i < 10; ++i)
{
int *tmp = &i;
s.emplace_back(tmp);
}
// 输出的结果全部为 10,因为循环退出时 i 等于10,tmp指向 i ,vector中的元素全部为 10
for(auto a : s)
{
std::cout << *a << std::endl;
}
return 0;
}
为了避免这种情况,我们需要对for循环中的局部指针变量每次使用 new 的方式初始化。
#include <iostream>
#include <vector>
int main()
{
std::vector<int*> s;
for(int i = 0; i < 10; ++i)
{
int *tmp = new int(i);
s.emplace_back(tmp);
}
// 这次输出是正常的
for(auto a : s)
{
std::cout << *a << std::endl;
}
return 0;
}
六、STL库中的二级配置器的未真正释放内存
STL库的二级配置器未真正释放内存,最后通过操作系统对链表中的内存进行回收,有细心的小伙伴可能会发现每次代码运行完,程序中可能会存在100多MB的内存未释放,这很可能就是二级配置器中开辟的内存,如果在容器存了10的9次方甚至更大的元素,这将是一个不小的内存,我对二级配置器做了小小的改变,增加了一个单链表记录开辟的内存的首地址,通过free函数进行释放。大家参考一下,自己 “抄写” 的STL的内存池在内存池的代码实现
static obj *volatile Pool; // 以单链表的形式记录分配的内存块,用以内存释放
static void free()
{
obj *cur = Pool, *next = nullptr;
while(cur)
{
next = cur->next;
std::free(cur);
cur = next;
}
Pool = nullptr;
}
七、柔性数组需要放在结构体最后
struct A
{
int a;
char b[];
};
八、内存池多线程安全
单纯的使用volatile不能保证多线程安全,volatile 保证的是变量安全,为了做到多线程安全,需要加锁和解锁操作。设计思路:通过模板传入参数 threads = true (挖的坑用上了😎),然后创建一个pthread_mutex_t 的包装类 thread,对于自由链表申请内存时加锁,完毕解锁。释放内存时,增加一个单链表,用来记录需要释放的内存单元,创建一个定时任务,每隔 10s,清空该链表。线程安全内存池的代码实现,这个项目不能说完全失败,对于某些需求来说可用。
这其实是一个悖论,因为内存池在设计的时候,本应在外部加锁的,就是向容器插入数据的时候加一下锁,这里用了一些方法,主要是避免频繁的加锁解锁。这确实是个悖论,对内存池上锁,在多线程状态下分配内存依然是单线程运行,单次安全性而言得到了保证,效率提升没有保证。
建议大家使用tcmalloc
或者jemalloc
内存优化总结: ptmalloc、tcmalloc 和 jemalloc
九、线程的detach和join
先看代码
#include <iostream>
#include <thread>
#include <windows.h> //sleep
using namespace std;
void t1() //普通的函数,用来执行线程
{
for (int i = 0; i < 10; ++i)
{
cout << "t1111\n";
Sleep(1);
}
}
void t2()
{
for (int i = 0; i < 20; ++i)
{
cout << "t22222\n";
Sleep(1);
}
}
int main()
{
thread th1(t1); //实例化一个线程对象th1,使用函数t1构造,然后该线程就开始执行了(t1())
thread th2(t2);
th1.join(); // 必须将线程join或者detach 等待子线程结束主进程才可以退出
th2.join();
//or use detach
//th1.detach();
//th2.detach();
cout << "here is main\n\n";
return 0;
}
这里调用join主线程会阻塞,直到子线程执行完,调用detach子线程分离和主线程分开执行,这样有一个问题,主线程不知道什么时候结束,可能子线程还没执行完,主线程就结束了。
另外传参的问题,向thread中传参数时,指针正常传参正常,引用传递子线程会拷贝一份实参
,所以如果实参是一个自定义的类对象,该类需要编写拷贝构造函数!!!此外,使用detach运行多线程时,可以传递参数的临时对象,这样在主线程结束之前,临时对象可以构建出来。
本节参考:
join和detach的注意事项
thread用法整理
detach使用隐患
十、返回值是函数中创建的对象
class A{
int a = 10;
};
A *func()
{
A a;
return &a;
}
A *func()
{
A *a = new A();
return a;
}
C++返回值是类对象会调用类的拷贝构造函数,所以要返回地址才能将函数中创建的对象返回!
十一、宏定义的妙用
#define TEST(a) #a
#define COUNT(typeName) \
typedef std::vector<typeName> typeName##Vec
#define vtrn64q(a, b) \
do { \
/* some code */ \
(a) = vcombine_u16(vtrn64_tmp_a0, vtrn64_tmp_a1); \
/* some more code */ \
} while (0)
int main()
{
int data = 10;
std::cout << TEST(data) << std::endl; // data
std::cout << COUNT(long) << std::endl;
return 0;
}
#
表示无论 a 代表变量名、函数名、类名,都将其翻译成字符串
##
表示将左右拼接形成一个新的变量名、类型名、函数名
(a)
确保 (a) 被视为一个整体,不会与其他代码冲突,这有助于提高宏的稳定性和可读性。在某些情况下,宏展开可能会导致不正确的结果,因为它不会将 (a) 视为一个整体,而是将 a 视为一个标识符,并可能与宏展开的其他代码发生冲突。
当用字符串拼接时,这样定义是最佳选择:
#define CONCAT_IMPL(a, b) a##b
#define CONCAT(a, b) CONCAT_IMPL(a, b)
通过 CONCAT_IMPL 的间接性,你可以确保在连接标识符时不会引起语法错误。
- 当 a、b 传参是宏时,只有通过再次展开才能正确替换宏的值,没有 CONCAT_IMPL,会展开成:
ttt_b
#define _b sss
CONCAT(ttt, _b);
- 当要展开多个时,没有 CONCAT_IMPL,第二个 CONCAT会被当成标识符,展开成:
CONCAT(ttt, sss)aaa
CONCAT(CONCAT(ttt, sss), aaa);
同时,#if
后面只能跟常量表达式,例如:宏,数字,逻辑运算,算术运算等
十二、函数模板的妙用
#include <iostream>
template<typename T, typename U>
T add(T a, U b) {
std::cout << a << " " << b << std::endl;
return a + b;
}
int main() {
int a = 5;
double b = 3.14;
int result1 = add<int>(a, b); // 使用部分参数 T=int,U=double
double result2 = add<double>(a, b); // 使用部分参数 T=int,U=double
int result3 = add<double>(a, b); // 使用部分参数 T=int,U=double
double result4 = add<int>(a, b); // 使用部分参数 T=int,U=double
return 0;
}
函数模板能被正确识别,所以可以放心传入模板参数和函数形参。
十三、typedef 起别名的用法
1.为变量定义别名
typedef int size;
2. 为 struct定义别名
struct POINT
{
int x;
int y;
}
typedef struct POINT PT;
typedef struct //这一行也可写作:typedef struct POINT
{
int x;
int y;
} PT;
3. 为函数定义别名
typedef int (*MYFUN)(int, int);
上面的例子定义MYFUN是一个函数指针, 函数类型是带两个int 参数, 返回一个int,
在分析这种形式的定义的时候可以用下面的方法:
先去掉typedef和别名,剩下的就是原变量的类型.
去掉typedef和MYFUN以后就剩:
int (*)(int, int)
类似的用法在NDK中有:
void *(*ATrace_beginSection) (const char* sectionName);
void *(*ATrace_endSection) (void);
typedef void *(*fp_ATrace_beginSection) (const char* sectionName);
typedef void *(*fp_ATrace_endSection) (void);
ATrace_beginSection = reinterpret_cast<fp_ATrace_beginSection>(
dlsym(lib, "ATrace_beginSection"));
ATrace_endSection = reinterpret_cast<fp_ATrace_endSection>(
dlsym(lib, "ATrace_endSection"));
如果不用 typedef 起别名,那么在 reinterpret_cast 强转的时候就需要把函数声明写出来,非常复杂
4. 为数组定义别名
语法:
typedef <类型关键字> <名称> <常量表达式>
typedef int arr[10];
typedef char CV_NO[1];
typedef char CV_YES[2];
本节参考:typedef用法与函数指针别名
十四、一种新的C++ struct成员变量定义方法
直接上代码了
#include <iostream>
struct Test
{
int len;
int tex;
int data;
};
int main(){
Test t = {.len = 10, .data = 5};
std::cout << t.len << " " << t.data << std::endl;
return 0;
}
总结
以后问题多了可以分模块,如果总结的有问题,请大家指出呀!!!
我只是一个练习两年半的实习生😂😂😂