5. Cạm bẫy thường gặp khi sử dụng mảng.
5.1 Cạm bẫy: Liên kết loại không an toàn.
OK, bạn đã được thông báo hoặc đã tự mình tìm ra rằng các quả cầu (biến phạm vi không gian tên có thể được truy cập bên ngoài đơn vị dịch) là Evil ™. Nhưng bạn có biết họ thật sự như thế nào không? Hãy xem xét chương trình bên dưới, bao gồm hai tệp [main.cpp] và [Numbers.cpp]:
// [main.cpp]
#include <iostream>
extern int* numbers;
int main()
{
using namespace std;
for( int i = 0; i < 42; ++i )
{
cout << (i > 0? ", " : "") << numbers[i];
}
cout << endl;
}
// [numbers.cpp]
int numbers[42] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
Trong Windows 7, phần này biên dịch và liên kết tốt với cả MinGW g ++ 4.4.1 và Visual C ++ 10.0.
Vì các loại không khớp, chương trình gặp sự cố khi bạn chạy nó.
Giải thích không chính thức: chương trình có Hành vi không xác định (UB), và thay vì bị sập, do đó, nó có thể bị treo, hoặc có thể không làm gì, hoặc có thể gửi e-mail đe dọa tới các tổng thống của Hoa Kỳ, Nga, Ấn Độ, Trung Quốc và Thụy Sĩ, và làm cho Da mũi bay ra khỏi mũi của bạn.
Giải thích trong thực tế: trong main.cpp
mảng được coi là một con trỏ, được đặt tại cùng địa chỉ với mảng. Đối với thực thi 32 bit, điều này có nghĩa là int
giá trị đầu tiên
trong mảng, được coi là một con trỏ. Ví dụ, trong main.cpp
các
numbers
biến chứa, hoặc xuất hiện để chứa, (int*)1
. Điều này làm cho chương trình truy cập bộ nhớ xuống dưới cùng của không gian địa chỉ, được bảo lưu theo quy ước và gây ra bẫy. Kết quả: bạn gặp sự cố.
Các trình biên dịch hoàn toàn nằm trong quyền của họ để không chẩn đoán lỗi này, vì C ++ 11 §3,5 / 10 nói, về yêu cầu của các loại tương thích cho các khai báo,
[N3290 §3.5 / 10]
Việc vi phạm quy tắc này đối với nhận dạng loại không yêu cầu chẩn đoán.
Đoạn văn tương tự chi tiết biến thể được phép:
Khai báo của một đối tượng mảng có thể chỉ định các kiểu mảng khác nhau bởi sự hiện diện hoặc vắng mặt của một mảng chính bị ràng buộc (8.3.4).
Biến thể được phép này không bao gồm khai báo tên dưới dạng một mảng trong một đơn vị dịch thuật và như một con trỏ trong một đơn vị dịch thuật khác.
5.2 Cạm bẫy: Thực hiện tối ưu hóa sớm ( memset
& bạn bè).
Chưa viết
5.3 Cạm bẫy: Sử dụng thành ngữ C để lấy số phần tử.
Với kinh nghiệm sâu sắc C, viết tự nhiên
#define N_ITEMS( array ) (sizeof( array )/sizeof( array[0] ))
Vì một array
phân rã thành con trỏ đến phần tử đầu tiên khi cần, biểu thức sizeof(a)/sizeof(a[0])
cũng có thể được viết là
sizeof(a)/sizeof(*a)
. Nó có nghĩa tương tự, và cho dù nó được viết như thế nào thì đó là thành ngữ C để tìm các phần tử số của mảng.
Cạm bẫy chính: thành ngữ C không an toàn. Ví dụ, mã số
#include <stdio.h>
#define N_ITEMS( array ) (sizeof( array )/sizeof( *array ))
void display( int const a[7] )
{
int const n = N_ITEMS( a ); // Oops.
printf( "%d elements.\n", n );
}
int main()
{
int const moohaha[] = {1, 2, 3, 4, 5, 6, 7};
printf( "%d elements, calling display...\n", N_ITEMS( moohaha ) );
display( moohaha );
}
chuyển một con trỏ tới N_ITEMS
, và do đó rất có thể tạo ra kết quả sai. Được biên dịch dưới dạng thực thi 32 bit trong Windows 7, nó tạo ra lỗi
7 yếu tố, hiển thị cuộc gọi ...
1 yếu tố.
- Trình biên dịch viết lại
int const a[7]
thành chỉ int const a[]
.
- Trình biên dịch viết lại
int const a[]
thành int const* a
.
N_ITEMS
do đó được gọi với một con trỏ.
- Đối với thực thi 32 bit
sizeof(array)
(kích thước của một con trỏ) thì 4.
sizeof(*array)
tương đương với sizeof(int)
, đối với thực thi 32 bit cũng là 4.
Để phát hiện lỗi này trong thời gian chạy, bạn có thể thực hiện
#include <assert.h>
#include <typeinfo>
#define N_ITEMS( array ) ( \
assert(( \
"N_ITEMS requires an actual array as argument", \
typeid( array ) != typeid( &*array ) \
)), \
sizeof( array )/sizeof( *array ) \
)
7 phần tử, hiển thị cuộc gọi ...
Xác nhận không thành công: ("N_ITEMS yêu cầu một mảng thực tế làm đối số", typeid (a)! = Typeid (& * a)), tệp runtime_detect ion.cpp, dòng 16
Ứng dụng này đã yêu cầu Runtime chấm dứt nó theo một cách khác thường.
Vui lòng liên hệ với nhóm hỗ trợ của ứng dụng để biết thêm thông tin.
Phát hiện lỗi thời gian chạy tốt hơn là không phát hiện, nhưng nó lãng phí một ít thời gian xử lý và có lẽ nhiều thời gian lập trình hơn. Tốt hơn với phát hiện tại thời gian biên dịch! Và nếu bạn không vui khi không hỗ trợ mảng các kiểu cục bộ với C ++ 98, thì bạn có thể làm điều đó:
#include <stddef.h>
typedef ptrdiff_t Size;
template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }
#define N_ITEMS( array ) n_items( array )
Biên dịch định nghĩa này được thay thế vào chương trình hoàn chỉnh đầu tiên, với g ++, tôi đã nhận được
M: \ Count> g ++ compile_time_detection.cpp
compile_time_detection.cpp: Trong hàm 'void display (const int *)':
compile_time_detection.cpp: 14: error: không có chức năng khớp nào để gọi tới 'n_items (const int * &)'
M: \ đếm> _
Cách thức hoạt động: mảng được truyền bằng tham chiếu đến n_items
, và do đó nó không phân rã thành con trỏ đến phần tử đầu tiên và hàm chỉ có thể trả về số lượng phần tử được chỉ định bởi loại.
Với C ++ 11, bạn cũng có thể sử dụng điều này cho các mảng kiểu cục bộ và đó là thành ngữ C ++ an toàn
để tìm số lượng phần tử của một mảng.
Cạm bẫy 5,4 C ++ 11 & C ++ 14: Sử dụng constexpr
hàm kích thước mảng.
Với C ++ 11 trở lên, điều đó là tự nhiên, nhưng như bạn sẽ thấy nguy hiểm!, Để thay thế chức năng C ++ 03
typedef ptrdiff_t Size;
template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }
với
using Size = ptrdiff_t;
template< class Type, Size n >
constexpr auto n_items( Type (&)[n] ) -> Size { return n; }
trong đó sự thay đổi đáng kể là việc sử dụng constexpr
, cho phép hàm này tạo ra hằng số thời gian biên dịch .
Ví dụ, trái ngược với hàm C ++ 03, hằng số thời gian biên dịch như vậy có thể được sử dụng để khai báo một mảng có cùng kích thước với một mảng khác:
// Example 1
void foo()
{
int const x[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 4};
constexpr Size n = n_items( x );
int y[n] = {};
// Using y here.
}
Nhưng hãy xem xét mã này bằng constexpr
phiên bản:
// Example 2
template< class Collection >
void foo( Collection const& c )
{
constexpr int n = n_items( c ); // Not in C++14!
// Use c here
}
auto main() -> int
{
int x[42];
foo( x );
}
Cạm bẫy: kể từ tháng 7 năm 2015, các biên dịch trên với MinGW-64 5.1.0 với
-pedantic-errors
, và, thử nghiệm với các trình biên dịch trực tuyến tại gcc.godbolt.org/ , cũng với clang 3.0 và clang 3.2, nhưng không phải với clang 3.3, 3.4. 1, 3.5.0, 3.5.1, 3.6 (RC1) hoặc 3.7 (thử nghiệm). Và quan trọng đối với nền tảng Windows, nó không biên dịch với Visual C ++ 2015. Lý do là một tuyên bố C ++ 11 / C ++ 14 về việc sử dụng các tham chiếu trong các constexpr
biểu thức:
C ++ 11 C ++ 14 $ 5,19 / 2 dấu gạch ngang
thứ chín
Một điều kiện thể hiện e
là một biểu thức hằng lõi trừ khi việc thẩm định e
, theo các quy tắc của máy trừu tượng (1.9), sẽ đánh giá một trong những biểu hiện sau:
⋮
- một biểu thức id đề cập đến một biến hoặc thành viên dữ liệu của kiểu tham chiếu trừ khi tham chiếu có khởi tạo trước và
- nó được khởi tạo với một biểu thức không đổi hoặc
- nó là một thành viên dữ liệu không tĩnh của một đối tượng có tuổi thọ bắt đầu trong quá trình đánh giá e;
Người ta luôn có thể viết dài dòng hơn
// Example 3 -- limited
using Size = ptrdiff_t;
template< class Collection >
void foo( Collection const& c )
{
constexpr Size n = std::extent< decltype( c ) >::value;
// Use c here
}
Sầu nhưng điều này thất bại khi Collection
không phải là một mảng thô.
Để đối phó với các tập hợp có thể không phải là mảng, người ta cần tính quá tải của
n_items
hàm, nhưng đối với thời gian biên dịch, người ta cần một biểu diễn thời gian biên dịch của kích thước mảng. Và giải pháp C ++ 03 cổ điển, cũng hoạt động tốt trong C ++ 11 và C ++ 14, là để cho hàm báo cáo kết quả của nó không phải là một giá trị mà thông qua loại kết quả chức năng của nó . Ví dụ như thế này:
// Example 4 - OK (not ideal, but portable and safe)
#include <array>
#include <stddef.h>
using Size = ptrdiff_t;
template< Size n >
struct Size_carrier
{
char sizer[n];
};
template< class Type, Size n >
auto static_n_items( Type (&)[n] )
-> Size_carrier<n>;
// No implementation, is used only at compile time.
template< class Type, size_t n > // size_t for g++
auto static_n_items( std::array<Type, n> const& )
-> Size_carrier<n>;
// No implementation, is used only at compile time.
#define STATIC_N_ITEMS( c ) \
static_cast<Size>( sizeof( static_n_items( c ).sizer ) )
template< class Collection >
void foo( Collection const& c )
{
constexpr Size n = STATIC_N_ITEMS( c );
// Use c here
(void) c;
}
auto main() -> int
{
int x[42];
std::array<int, 43> y;
foo( x );
foo( y );
}
Về lựa chọn loại trả về cho static_n_items
: mã này không sử dụng std::integral_constant
vì với std::integral_constant
kết quả được biểu thị trực tiếp dưới dạng constexpr
giá trị, giới thiệu lại vấn đề ban đầu. Thay vì một Size_carrier
lớp, người ta có thể để hàm trực tiếp trả về một tham chiếu đến một mảng. Tuy nhiên, không phải ai cũng quen thuộc với cú pháp đó.
Về cách đặt tên: một phần của giải pháp này cho constexpr
vấn đề -invalid-do-tham chiếu là làm cho sự lựa chọn của thời gian biên dịch không đổi rõ ràng.
Hy vọng rằng constexpr
vấn đề liên quan đến vấn đề của bạn sẽ được khắc phục với C ++ 17, nhưng cho đến khi đó, một macro như các STATIC_N_ITEMS
trình biên dịch trên mang lại tính di động, ví dụ như trình biên dịch clang và Visual C ++, kiểu giữ lại sự an toàn.
Liên quan: macro không tôn trọng phạm vi, vì vậy để tránh xung đột tên nó có thể là một ý tưởng tốt để sử dụng một tiền tố tên, ví dụ MYLIB_STATIC_N_ITEMS
.