自己常用的C/C++小技巧[3]
自己常用的小技巧
本文github備份地址
這裡列出了自己常用的一些c/c++小技巧, 有些會有不足, 可以簡單探討一下.
顏色混合
分類: 小技巧
有一個可能很常用的操作, 或者說函數: 平均數. 通過基礎數學我們可以知道(a+b)/2
, 如果用c中無符號整型表示:
uint32_t avg(uint32_t a, uint32_t b) {
return (a+b)/2;
}
一般情況下沒問題, 但是在兩個數足夠大時可能會產生溢出, 由於乘法分配律可知, 我們可以實現為:
uint32_t avg(uint32_t a, uint32_t b) {
return a/2 + b/2;
}
但是我們操作的是無符號整型, 兩個奇數一操作最低位就被忽略了, 所以我們可以利用位操作, 再加上除以2可以化作右移一位, 我們可以實現為:
uint32_t avg(uint32_t a, uint32_t b) {
return (a>>1) + (b>>1) + (a&b&1);
}
現在我們可以推廣至顏色上, 我們現在常用的是R8G8B8X8格式, X表示忽略或者表示不透明度. 如果現在我們規定低到高位分別是RGB, 最高8位忽略, 並假設為0:
#define R8G8B8_MASK_HI ((uint32_t)0x00FEFEFE)
#define R8G8B8_MASK_LO ((uint32_t)0x00010101)
uint32_t avg_r8g8b8(uint32_t a, uint32_t b) {
return ((a&R8G8B8_MASK_HI)>>1) + ((b&R8G8B8_MASK_HI)>>1) + (a&b&R8G8B8_MASK_LO);
}
再推廣一下, 以後我們可能覺得24bit顏色太辣雞, 推廣到30bit. 我們現在使用R10G10B10X2格式, 這個就可以修改為:
#define R10G10B10_MASK_HI ((uint32_t)0x3FEFFBFE)
#define R10G10B10_MASK_LO ((uint32_t)0x00100401)
uint32_t avg_r10g10b10(uint32_t a, uint32_t b) {
return ((a&R10G10B10_MASK_HI) >> 1) + ((b&R8G8B8_MASK_HI) >> 1) + (a&b&R8G8B8_MASK_LO);
}
不過實際上主要是不透明度的原因, 這個方法在用於包含Alpha信息的顏色時, 會由於Alpha權重問題出現問題. 這個問題引申出一個"預乘Alpha"的概念, 自己被這個東西坑過, 真是記憶猶新。
合併申請
分類: 優化技巧
這個技巧很簡單, 將多次內存申請合併到一次, 優點為:
- 提高效率, 內存申請一般來說是比較耗時的操作, 合併後效率提升是很自然的
- 減少邏輯分支, 如果需要申請兩塊內存, 錯誤路徑會有三個, 其中兩個特殊路徑還要去釋放已經申請的內存. 合併後錯誤路徑就只有一個.
void fx0(void) {
void* a = malloc(100);
void* b = malloc(200);
if (a && b) {
// XXXXX
}
// 雖然free接受空指針, 實際上可能會有類似析構的操作, 還是需要判斷
if (a) free(a);
if (b) free(b);
}
void fx1(void) {
void* a = malloc(100);
if (a) {
void* b = malloc(200);
if (b) {
// XXXXXXXXXXXXXX
free(b);
}
free(a);
}
}
void gx(void) {
void* a = malloc(300);
if (a) {
void* b = (char*)a + 100;
// XXXXXXXXXXXXXX
free(a);
}
}
缺點, 不適合處理大空間申請, 比如申請兩個250MB的內存最好分別申請, 不然地址空間上可能找不到500MB連續空間, 但是能找到兩個250MB的連續空間.
還有一個小缺點就是, 現在的運行庫配合IDE能夠在越界後進行報錯. 假如合併, 前者越界會寫入下一段空間, 這個非常危險, 並且比較難調試(所以越界檢測是非常重要的, 建議使用大量的斷言進行處理).
雖然這個技巧很簡單, 但是實際上需要直接申請兩個獨立部分可能比較少見, 常見是"隱式"地多次申請:
struct A {
B* b = new(std::nothrow) B;
C* c = new(std::nothrow) C;
};
struct D {
B* b = new(std::nothrow) B;
};
int main() {
A a;
delete(new(std::nothrow) D);
}
這裡列舉兩種情況, A
情況就是上述比較明顯的, 擁有兩次顯式的new
, 而D
的情況則是隱式的兩次. 有一點就是我們可以通過replacement new
和直接調用析構函數
進行內存的再利用, 但是非常注意的是內存對齊的情況, c++目前並沒有將c11的aligned_alloc
納入標準(即便納入按照微軟的脾氣還是不可能在VS上實現, 主要是允許free
掉aligned_alloc
的), 申請對齊的內存需要自己處理又無形地增加了複雜度(好在一般按照8位元組對齊就行, 除了特殊要求).
有時會我們會經常的使用"懸浮"狀態的對象, 就像D
這種, 完全手動控制生命周期的, 其中隱含的多次申請依然可以合併, 只需要找准偏移量即可.
柔性數組, 或者說零長數組(Zero-Length Array) 其實就是這個的一個體現:
struct array_t {
size_t length;
char data[0];
};
其中類c++擴展是[0]
, c99標準是[]
. c++中, 有時候為了避免兼容問題, 會不寫成員變數, 取而代之的是成員函數:
struct array_t {
size_t length;
char* data() { return reinterpret_cast<char*>(this+1) }
};
數組多態
分類: 內部優化
數組一般來說就是儲存同一類型的數據, 如果需要多態, 又要保持隨機訪問可能的就是儲存指針數組. 不過內部使用或者強制規定的話, 我們可以規定一個上限, 各個類型的較大值.
enum : size_t { THIS_MAX_SIZE = 256, THIS_MAX_COUNT = 16 };
class Factory {
template<typename T> T* get_at(size_t i) {
assert(i < m_count && "out of range");
return reinterpret_cast<T*>(m_buffer + THIS_MAX_SIZE * i);
}
size_t m_count;
uint8_t m_buffer[THIS_MAX_SIZE*THIS_MAX_COUNT];
};
類似這樣, 其實就是上一條: 合併申請. 然後手動控制對象構造(replacement new
)與析構(調用析構函數
), 對於"內部優化"來說, 是允許的.
推薦閱讀: