C++的语法问题总结


前言

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

四、类中成员变量包含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;
}

总结

以后问题多了可以分模块,如果总结的有问题,请大家指出呀!!!
我只是一个练习两年半的实习生😂😂😂

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值