Câu trả lời:
Với memcpy
, đích không được trùng với nguồn. Với memmove
nó có thể. Điều này có nghĩa là memmove
có thể chậm hơn một chút so vớimemcpy
vì nó không thể đưa ra các giả định tương tự.
Ví dụ: memcpy
có thể luôn sao chép địa chỉ từ thấp đến cao. Nếu đích trùng lặp sau nguồn, điều này có nghĩa là một số địa chỉ sẽ bị ghi đè trước khi sao chép. memmove
sẽ phát hiện điều này và sao chép theo hướng khác - từ cao xuống thấp - trong trường hợp này. Tuy nhiên, việc kiểm tra điều này và chuyển sang thuật toán khác (có thể kém hiệu quả hơn) cần nhiều thời gian.
i = i++ + 1
là không xác định; trình biên dịch không cấm bạn viết chính xác mã đó nhưng kết quả của lệnh đó có thể là bất kỳ thứ gì và các trình biên dịch hoặc CPU khác nhau sẽ hiển thị các giá trị khác nhau ở đây.
memmove
có thể xử lý bộ nhớ chồng chéo, memcpy
không thể.
Xem xét
char[] str = "foo-bar";
memcpy(&str[3],&str[4],4); //might blow up
Rõ ràng nguồn và đích bây giờ trùng nhau, chúng tôi đang ghi đè "-bar" bằng "bar". Đó là hành vi không xác định bằng cách sử dụng memcpy
nếu nguồn và đích trùng nhau, vì vậy trong trường hợp này, chúng tôi cần memmove
.
memmove(&str[3],&str[4],4); //fine
Sự khác biệt chính giữa memmove()
và memcpy()
là trong memmove()
một bộ đệm - bộ nhớ tạm thời - được sử dụng, vì vậy không có nguy cơ chồng chéo. Mặt khác, memcpy()
sao chép trực tiếp dữ liệu từ vị trí được nguồn trỏ đến vị trí được trỏ bởi đích . ( http://www.cplusplus.com/reference/cstring/memcpy/ )
Hãy xem xét các ví dụ sau:
#include <stdio.h>
#include <string.h>
int main (void)
{
char string [] = "stackoverflow";
char *first, *second;
first = string;
second = string;
puts(string);
memcpy(first+5, first, 5);
puts(first);
memmove(second+5, second, 5);
puts(second);
return 0;
}
Như bạn mong đợi, điều này sẽ in ra:
stackoverflow
stackstacklow
stackstacklow
Nhưng trong ví dụ này, kết quả sẽ không giống nhau:
#include <stdio.h>
#include <string.h>
int main (void)
{
char string [] = "stackoverflow";
char *third, *fourth;
third = string;
fourth = string;
puts(string);
memcpy(third+5, third, 7);
puts(third);
memmove(fourth+5, fourth, 7);
puts(fourth);
return 0;
}
Đầu ra:
stackoverflow
stackstackovw
stackstackstw
Đó là bởi vì "memcpy ()" thực hiện như sau:
1. stackoverflow
2. stacksverflow
3. stacksterflow
4. stackstarflow
5. stackstacflow
6. stackstacklow
7. stackstacksow
8. stackstackstw
memmove()
bắt buộc phải sử dụng bộ đệm. Nó hoàn toàn có quyền di chuyển tại chỗ (miễn là mỗi lần đọc hoàn thành trước khi ghi vào cùng một địa chỉ).
Giả sử bạn sẽ phải triển khai cả hai, việc triển khai có thể trông như thế:
void memmove ( void * dst, const void * src, size_t count ) {
if ((uintptr_t)src < (uintptr_t)dst) {
// Copy from back to front
} else if ((uintptr_t)dst < (uintptr_t)src) {
// Copy from front to back
}
}
void mempy ( void * dst, const void * src, size_t count ) {
if ((uintptr_t)src != (uintptr_t)dst) {
// Copy in any way you want
}
}
Và điều này sẽ giải thích khá rõ sự khác biệt. memmove
luôn sao chép theo cách mà nó vẫn an toàn nếu src
và dst
chồng chéo lên nhau, trong khi memcpy
không quan tâm như tài liệu hướng dẫn khi sử dụng memcpy
, hai vùng bộ nhớ không được chồng lên nhau.
Ví dụ: nếu memcpy
bản sao "từ trước ra sau" và các khối bộ nhớ được căn chỉnh như thế này
[---- src ----]
[---- dst ---]
sao chép byte đầu tiên của src
để dst
đã phá hủy nội dung của byte cuối cùng củasrc
trước khi chúng được sao chép. Chỉ sao chép "từ trước ra trước" mới dẫn đến kết quả chính xác.
Bây giờ hoán đổi src
và dst
:
[---- dst ----]
[---- src ---]
Trong trường hợp đó, chỉ an toàn khi sao chép "từ trước ra sau" vì sao chép "từ sau ra trước" sẽ phá hủy src
gần phía trước của nó khi sao chép byte đầu tiên.
Bạn có thể nhận thấy rằng việc memmove
triển khai ở trên thậm chí không kiểm tra xem chúng có thực sự chồng chéo hay không, nó chỉ kiểm tra vị trí tương đối của chúng, nhưng chỉ điều đó sẽ làm cho bản sao an toàn. Như memcpy
thường sử dụng cách nhanh nhất có thể để sao chép bộ nhớ trên bất kỳ hệ thống nào, memmove
thường được thực hiện như:
void memmove ( void * dst, const void * src, size_t count ) {
if ((uintptr_t)src < (uintptr_t)dst
&& (uintptr_t)src + count > (uintptr_t)dst
) {
// Copy from back to front
} else if ((uintptr_t)dst < (uintptr_t)src
&& (uintptr_t)dst + count > (uintptr_t)src
) {
// Copy from front to back
} else {
// They don't overlap for sure
memcpy(dst, src, count);
}
}
Đôi khi, nếu memcpy
luôn sao chép "từ trước ra sau" hoặc "sau ra trước", memmove
cũng có thể sử dụng memcpy
trong một trong các trường hợp chồng chéo nhưng memcpy
thậm chí có thể sao chép theo cách khác tùy thuộc vào cách dữ liệu được căn chỉnh và / hoặc lượng dữ liệu được đã sao chép, vì vậy ngay cả khi bạn đã thử nghiệm như thế nàomemcpy
sao chép trên hệ thống của mình, bạn không thể dựa vào kết quả kiểm tra đó để luôn chính xác.
Điều đó có ý nghĩa gì đối với bạn khi quyết định gọi cái nào?
Trừ khi bạn biết chắc điều đó src
và dst
không trùng lặp, hãy gọi memmove
vì nó sẽ luôn dẫn đến kết quả chính xác và thường là nhanh nhất có thể cho trường hợp sao chép bạn yêu cầu.
Nếu bạn biết chắc chắn điều đó src
và dst
không trùng lặp, hãy gọi memcpy
vì nó không quan trọng bạn gọi kết quả nào, cả hai sẽ hoạt động chính xác trong trường hợp đó, nhưng memmove
sẽ không bao giờ nhanh hơn memcpy
và nếu bạn không may mắn, nó thậm chí có thể chậm hơn, vì vậy bạn chỉ có thể thắng cuộc gọi memcpy
.
chỉ đơn giản từ tiêu chuẩn ISO / IEC: 9899, nó được mô tả tốt.
7.21.2.1 Chức năng ghi nhớ
[...]
2 Hàm memcpy sao chép n ký tự từ đối tượng được trỏ tới bởi s2 vào đối tượng được trỏ tới bởi s1. Nếu việc sao chép diễn ra giữa các đối tượng chồng lên nhau, hành vi đó là không xác định.
Và
7.21.2.2 Chức năng ghi nhớ
[...]
2 Hàm memmove sao chép n ký tự từ đối tượng được trỏ tới bởi s2 vào đối tượng được trỏ tới bởi s1. Việc sao chép diễn ra như thể n ký tự từ đối tượng được trỏ đến bởi s2 lần đầu tiên được sao chép vào một mảng tạm thời gồm n ký tự không chồng lên các đối tượng được trỏ đến bởi s1 và s2, sau đó n ký tự từ mảng tạm thời được sao chép vào vật được chỉ bởi s1.
Tôi thường sử dụng cái nào cho câu hỏi, tùy thuộc vào chức năng mà tôi cần.
Trong văn bản thuần túy memcpy()
không cho phép s1
và s2
chồng chéo, trong khi memmove()
đó.
Có hai cách rõ ràng để triển khai mempcpy(void *dest, const void *src, size_t n)
(bỏ qua giá trị trả về):
for (char *p=src, *q=dest; n-->0; ++p, ++q)
*q=*p;
char *p=src, *q=dest;
while (n-->0)
q[n]=p[n];
Trong lần triển khai đầu tiên, việc sao chép tiến hành từ địa chỉ thấp đến cao và trong lần thứ hai, từ cao xuống thấp. Nếu phạm vi được sao chép trùng lặp (ví dụ như trường hợp khi cuộn bộ đệm khung), thì chỉ một hướng hoạt động là đúng và hướng khác sẽ ghi đè lên các vị trí mà sau đó sẽ được đọc từ đó.
Một memmove()
triển khai, đơn giản nhất, sẽ kiểm tra dest<src
(theo một số cách phụ thuộc vào nền tảng) và thực hiện hướng thích hợp của memcpy()
.
Tất nhiên, mã người dùng không thể làm điều đó, bởi vì ngay cả sau khi truyền src
và dst
đến một số loại con trỏ cụ thể, chúng (nói chung) không trỏ vào cùng một đối tượng và do đó không thể so sánh được. Nhưng thư viện tiêu chuẩn có thể có đủ kiến thức nền tảng để thực hiện so sánh như vậy mà không gây ra Hành vi không xác định.
Lưu ý rằng trong cuộc sống thực, việc triển khai có xu hướng phức tạp hơn đáng kể, để đạt được hiệu suất tối đa từ việc truyền tải lớn hơn (khi sự liên kết cho phép) và / hoặc sử dụng tốt bộ đệm dữ liệu. Đoạn mã trên chỉ là để làm cho điểm đơn giản nhất có thể.
memmove có thể xử lý các vùng nguồn và đích chồng chéo, trong khi memcpy không thể. Trong số hai, memcpy hiệu quả hơn nhiều. Vì vậy, tốt hơn hãy SỬ DỤNG memcpy nó nếu bạn có thể.
Tham khảo: https://www.youtube.com/watch?v=Yr1YnOVG-4g Tiến sĩ Jerry Cain, (Bài giảng Hệ thống nội bộ của Stanford - 7) Thời gian: 36:00
memcpy()
và không phải memcopy()
.